diff --git a/README.md b/README.md
index 92e95da..04aecd6 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ This package provides native Go functionality to parse an existing EXIF block, u
 To get the project and dependencies:
 
 ```
-$ go get -t github.com/dsoprea/go-exif
+$ go get -t github.com/dsoprea/go-exif/v2
 ```
 
 
@@ -26,15 +26,15 @@ $ go get -t github.com/dsoprea/go-exif
 The traditional method:
 
 ```
-$ go test github.com/dsoprea/go-exif
+$ go test github.com/dsoprea/go-exif/v2
 ```
 
 
 ## Usage
 
-The package provides a set of [working examples](https://godoc.org/github.com/dsoprea/go-exif#pkg-examples) and is covered by unit-tests. Please look to these for getting familiar with how to read and write EXIF.
+The package provides a set of [working examples](https://godoc.org/github.com/dsoprea/go-exif/v2#pkg-examples) and is covered by unit-tests. Please look to these for getting familiar with how to read and write EXIF.
 
-In general, this package is concerned only with parsing and encoding raw EXIF data. It does not understand specific file-formats. This package assumes you know how to extract the raw EXIF data from a file, such as a JPEG, and, if you want to update it, know then how to write it back. File-specific formats are not the concern of *go-exif*, though we provide [exif.SearchAndExtractExif](https://godoc.org/github.com/dsoprea/go-exif#SearchAndExtractExif) and [exif.SearchFileAndExtractExif](https://godoc.org/github.com/dsoprea/go-exif#SearchFileAndExtractExif) as brute-force search mechanisms that will help you explore the EXIF information for newer formats that you might not yet have any way to parse.
+In general, this package is concerned only with parsing and encoding raw EXIF data. It does not understand specific file-formats. This package assumes you know how to extract the raw EXIF data from a file, such as a JPEG, and, if you want to update it, know then how to write it back. File-specific formats are not the concern of *go-exif*, though we provide [exif.SearchAndExtractExif](https://godoc.org/github.com/dsoprea/go-exif/v2#SearchAndExtractExif) and [exif.SearchFileAndExtractExif](https://godoc.org/github.com/dsoprea/go-exif/v2#SearchFileAndExtractExif) as brute-force search mechanisms that will help you explore the EXIF information for newer formats that you might not yet have any way to parse.
 
 That said, the author also provides [go-jpeg-image-structure](https://github.com/dsoprea/go-jpeg-image-structure) and [go-png-image-structure](https://github.com/dsoprea/go-png-image-structure) to support properly reading and writing JPEG and PNG images. See the [SetExif example in go-jpeg-image-structure](https://godoc.org/github.com/dsoprea/go-jpeg-image-structure#example-SegmentList-SetExif) for practical information on getting started with JPEG files.
 
@@ -55,8 +55,7 @@ There is an "IFD mapping" and a "tag index" that must be created and passed to t
 There is a reader implementation included as a runnable tool:
 
 ```
-$ go get github.com/dsoprea/go-exif/exif-read-tool
-$ go build -o exif-read-tool github.com/dsoprea/go-exif/exif-read-tool
+$ go get github.com/dsoprea/go-exif/v2/exif-read-tool
 $ exif-read-tool -filepath "<media file-path>"
 ```
 
diff --git a/v2/LICENSE b/v2/LICENSE
new file mode 100644
index 0000000..0b9358a
--- /dev/null
+++ b/v2/LICENSE
@@ -0,0 +1,9 @@
+MIT LICENSE
+
+Copyright 2019 Dustin Oprea
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/v2/assets/DC-008-2012_E.pdf b/v2/assets/DC-008-2012_E.pdf
new file mode 100644
index 0000000..e43d85c
Binary files /dev/null and b/v2/assets/DC-008-2012_E.pdf differ
diff --git a/v2/assets/DC-008-Translation-2016-E.pdf b/v2/assets/DC-008-Translation-2016-E.pdf
new file mode 100644
index 0000000..c05df44
Binary files /dev/null and b/v2/assets/DC-008-Translation-2016-E.pdf differ
diff --git a/v2/assets/NDM_8901.jpg b/v2/assets/NDM_8901.jpg
new file mode 100644
index 0000000..b892262
Binary files /dev/null and b/v2/assets/NDM_8901.jpg differ
diff --git a/v2/assets/NDM_8901.jpg.exif b/v2/assets/NDM_8901.jpg.exif
new file mode 100644
index 0000000..58ec96f
Binary files /dev/null and b/v2/assets/NDM_8901.jpg.exif differ
diff --git a/v2/assets/NDM_8901.jpg.thumbnail b/v2/assets/NDM_8901.jpg.thumbnail
new file mode 100644
index 0000000..e82002b
Binary files /dev/null and b/v2/assets/NDM_8901.jpg.thumbnail differ
diff --git a/v2/assets/PNG-GROUP-DRAFT PNG proposed eXIf chunk, draft 2017-06-15.pdf b/v2/assets/PNG-GROUP-DRAFT PNG proposed eXIf chunk, draft 2017-06-15.pdf
new file mode 100644
index 0000000..ad6405f
Binary files /dev/null and b/v2/assets/PNG-GROUP-DRAFT PNG proposed eXIf chunk, draft 2017-06-15.pdf differ
diff --git a/v2/assets/exif_read.json b/v2/assets/exif_read.json
new file mode 100644
index 0000000..18e8a4b
--- /dev/null
+++ b/v2/assets/exif_read.json
@@ -0,0 +1,695 @@
+[
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 0,
+        "tag_id": 271,
+        "tag_name": "Make",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 6,
+        "value": "Canon",
+        "value_string": "Canon"
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 0,
+        "tag_id": 272,
+        "tag_name": "Model",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 22,
+        "value": "Canon EOS 5D Mark III",
+        "value_string": "Canon EOS 5D Mark III"
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 0,
+        "tag_id": 274,
+        "tag_name": "Orientation",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "1",
+        "value_string": "1"
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 0,
+        "tag_id": 282,
+        "tag_name": "XResolution",
+        "tag_type_id": 5,
+        "tag_type_name": "RATIONAL",
+        "unit_count": 1,
+        "value": "72/1",
+        "value_string": "72/1"
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 0,
+        "tag_id": 283,
+        "tag_name": "YResolution",
+        "tag_type_id": 5,
+        "tag_type_name": "RATIONAL",
+        "unit_count": 1,
+        "value": "72/1",
+        "value_string": "72/1"
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 0,
+        "tag_id": 296,
+        "tag_name": "ResolutionUnit",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "2",
+        "value_string": "2"
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 0,
+        "tag_id": 306,
+        "tag_name": "DateTime",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 20,
+        "value": "2017:12:02 08:18:50",
+        "value_string": "2017:12:02 08:18:50"
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 0,
+        "tag_id": 315,
+        "tag_name": "Artist",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 1,
+        "value": "",
+        "value_string": ""
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 0,
+        "tag_id": 531,
+        "tag_name": "YCbCrPositioning",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "2",
+        "value_string": "2"
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 0,
+        "tag_id": 33432,
+        "tag_name": "Copyright",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 1,
+        "value": "",
+        "value_string": ""
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 0,
+        "tag_id": 34665,
+        "tag_name": "ExifTag",
+        "tag_type_id": 4,
+        "tag_type_name": "LONG",
+        "unit_count": 1,
+        "value": "360",
+        "value_string": "360"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 33434,
+        "tag_name": "ExposureTime",
+        "tag_type_id": 5,
+        "tag_type_name": "RATIONAL",
+        "unit_count": 1,
+        "value": "1/640",
+        "value_string": "1/640"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 33437,
+        "tag_name": "FNumber",
+        "tag_type_id": 5,
+        "tag_type_name": "RATIONAL",
+        "unit_count": 1,
+        "value": "4/1",
+        "value_string": "4/1"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 34850,
+        "tag_name": "ExposureProgram",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "4",
+        "value_string": "4"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 34855,
+        "tag_name": "ISOSpeedRatings",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "1600",
+        "value_string": "1600"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 34864,
+        "tag_name": "SensitivityType",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "2",
+        "value_string": "2"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 34866,
+        "tag_name": "RecommendedExposureIndex",
+        "tag_type_id": 4,
+        "tag_type_name": "LONG",
+        "unit_count": 1,
+        "value": "1600",
+        "value_string": "1600"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 36864,
+        "tag_name": "ExifVersion",
+        "tag_type_id": 7,
+        "tag_type_name": "UNDEFINED",
+        "unit_count": 4,
+        "value": "0230",
+        "value_string": "0230"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 36867,
+        "tag_name": "DateTimeOriginal",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 20,
+        "value": "2017:12:02 08:18:50",
+        "value_string": "2017:12:02 08:18:50"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 36868,
+        "tag_name": "DateTimeDigitized",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 20,
+        "value": "2017:12:02 08:18:50",
+        "value_string": "2017:12:02 08:18:50"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 37121,
+        "tag_name": "ComponentsConfiguration",
+        "tag_type_id": 7,
+        "tag_type_name": "UNDEFINED",
+        "unit_count": 4,
+        "value": {
+            "ConfigurationId": 2,
+            "ConfigurationBytes": "AQIDAA=="
+        },
+        "value_string": "ComponentsConfiguration\u003cID=[YCBCR] BYTES=[1 2 3 0]\u003e"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 37377,
+        "tag_name": "ShutterSpeedValue",
+        "tag_type_id": 10,
+        "tag_type_name": "SRATIONAL",
+        "unit_count": 1,
+        "value": "614400/65536",
+        "value_string": "614400/65536"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 37378,
+        "tag_name": "ApertureValue",
+        "tag_type_id": 5,
+        "tag_type_name": "RATIONAL",
+        "unit_count": 1,
+        "value": "262144/65536",
+        "value_string": "262144/65536"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 37380,
+        "tag_name": "ExposureBiasValue",
+        "tag_type_id": 10,
+        "tag_type_name": "SRATIONAL",
+        "unit_count": 1,
+        "value": "0/1",
+        "value_string": "0/1"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 37383,
+        "tag_name": "MeteringMode",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "5",
+        "value_string": "5"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 37385,
+        "tag_name": "Flash",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "16",
+        "value_string": "16"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 37386,
+        "tag_name": "FocalLength",
+        "tag_type_id": 5,
+        "tag_type_name": "RATIONAL",
+        "unit_count": 1,
+        "value": "16/1",
+        "value_string": "16/1"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 37500,
+        "tag_name": "MakerNote",
+        "tag_type_id": 7,
+        "tag_type_name": "UNDEFINED",
+        "unit_count": 8152,
+        "value": {
+            "MakerNoteType": "KAABAAMAMQAAAHQFAAACAAMABAA=",
+            "MakerNoteBytes": "KAABAAMAMQAAAHQFAAACAAMABAAAANYFAAADAAMABAAAAN4FAAAEAAMAIgAAAOYFAAAGAAIAFgAAACoGAAAHAAIAGAAAAEoGAAAJAAIAIAAAAGIGAAANAAcAAAYAAIIGAAAQAAQAAQAAAIUCAIATAAMABAAAAIIMAAAZAAMAAQAAAAEAAAAmAAMACQEAAIoMAAA1AAQABAAAAJwOAACTAAMAHgAAAKwOAACVAAIASgAAAOgOAACWAAIAEAAAADIPAACXAAcAAAQAAEIPAACYAAMABAAAAEITAACZAAQAUwAAAEoTAACaAAQABQAAAJYUAACgAAMADgAAAKoUAACqAAMABgAAAMYUAAC0AAMAAQAAAAEAAADQAAQAAQAAAAAAAADgAAMAEQAAANIUAAABQAMAIAUAAPQUAAAIQAMAAwAAADQfAAAJQAMAAwAAADofAAAQQAIAIAAAAEAfAAARQAcA/AAAAGAfAAASQAIAIAAAAFwgAAATQAQACwAAAHwgAAAVQAcAyAEAAKggAAAWQAQABwAAAHAiAAAYQAQABwAAAIwiAAAZQAcAHgAAAKgiAAAhQAQABQAAAMYiAAAlQAQACQAAANoiAAAnQAQABQAAAP4iAAAoQAQAEwAAABIjAAAAAAAAYgACAAAAAwAAAAQAAAAAAAAAAQABAAEAAAAAAAAA/38PAAMAAgAAAAIA///7ASMAEAABAIAAIAEAAAAAAAAAAP///////wAAAAAAAAAA/////wAAAAD/f////////wAA//8AABAAhdLMLwAAAAAAAAAARAAAACABkACAACwBAAAAAAMAAAAIAAgAmQAAAAAAAAAAAAAAAQAAAAAAhAAsAX0AAAAAAPgA//////////8AAAAAAABDYW5vbiBFT1MgNUQgTWFyayBJSUkAAAAAAAAAAAAAAEZpcm13YXJlIFZlcnNpb24gMS4zLjQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqqqDKYMoaAABeAF0AAMAAAAAAAABAAAGAAAAmZoAYQB9AHwAEAAAAAIAAAAAAwAAAAAAAAAQAAABu7tqQP/6QIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQP8AagArAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAEAAAAAAAAAAAAAAAAAAAD//wHWBB8fAAAAAAAA/////wzMzAEAAAADAAAABQAAAAAAAAAFAAAAAGoAAAAAagAAAAAA+BEAAAABAAEAAQAAAQAAAAEAAAABAAAAAwAAAAMAAAADAAAAAAAAAAEAAAAAAAAAAAAAAIcAAAABAAAAAQAAAAEAAAAHAAAAAAAAAwUD//8DBwAAAAAAAAcAAAAAAAAWAQAAAgAFAAAAAAAEAAkAAAAAAAEACQEAAAAAAAAABgEAAAcDAwMC/wAAAAAAAAAAAShQAfsAEAAjkXeab4j/AAAAAAAkAAAQaAEABgcpAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAIAWAAAADwAAABIAAIAKAABAAgAAAAMAANACAADgAQAAAAAAAAAAAADQAgAA4AEAANACAADgAQAAAAAAAAAAAADQAgAA4AEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////////////////////8AAgAAAAAAAAAAAAAKAgABAAEAAAACAQAAAAAAAAIBAQEAAAAAAAAAAAAAAAAAAAAxLjMuNABBQSgzNCkAVlEAGAKYGVj+EwBk7gAAZO4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIMAAABkAAAAZAAAAMQbAADEIgAAAAAAAIQAAABlAAAAZAAAAAgAAAAIAAAACAAAAAgAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADrAQAEAAR+AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOsBAAQABH4CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6wEABAAEfgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADrAQAEAAR+AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOsBAAQABH4CAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAwAAAAAAAAAAAAAA776t3u++rd4AAAAAAgAAAAAAAAAAAAAA776t3u++rd4AAAAABAAAAAAAAAAAAAAA776t3u++rd4AAAAAAAAAAAAAAAAAAAAA776t3u++rd4AAAAAAAAAAAAAAAAAAAAA776t3u++rd4AAAAAAwAAAO++rd7vvq3eAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAA776t3u++rd4AAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAACBAIEAgQAAAP//////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAEAAQAAAAAAAAAAOphIloAAAAAAgAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAMA2AADEOQEAAAAAAAAAAAAAAAAAAwAAAFMAAABYAAOAAAAAAAMAAABNAAAATgAAAAAAAAADAAAAMwAAADsAA4AAAAAAAwAAAFQAAACfAAcAcAASAgIAPQA9AAAPAAoADwAKfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB+AH4AfgB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AH0AfQB9AMAAAABA//UDNQN1AqoBwAAAAAv8y/yL/Vb+QP/1AzUDdQKqAcAAAAAL/Mv8i/1W/kD/9QM1A3UCqgHAAAAAC/zL/Iv9Vv5A//UDNQN1AqoBwAAAAAv8y/yL/Vb+QP/1AzUDdQKqAcAAAAAL/Mv8i/1W/kD/wAAAAED/UP5Q/lD+3/7f/t/+3/7f/t/+3/7f/t/+3/7f/nH/cf9x/3H/cf9x/3H/cf9x/3H/cf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAjwCPAI8AjwCPAI8AjwCPAI8AjwCPACEBIQEhASEBIQEhASEBIQEhASEBIQGwAbABsAEAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAD//xAAAADgAQAACAAAADwAAAA8AAAAAAAAAAAAAAAAAAAA//8AAAAAAAAAAAAA/////2EA//8AAAAA///WAQAAAAAAAAAA//8AAB8AHwBFRjE2LTM1bW0gZi80TCBJUyBVU00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//9IzEFEMDQxMzg5NQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABMAQAAAwAAAAEAAABUAAAABgAAAAEBAAABAAAAAAAAAAIBAAABAAAAAAAAAAQBAAABAAAAAAAAAAUBAAABAAAAAAAAAAYBAAACAAAAAwAAAAAAAAAIAQAAAQAAAAAAAAACAAAAIAAAAAIAAAAKBAAAAQAAAAcAAAALBAAAAQAAAAAAAAAEAAAAxAAAAAUAAAAMBwAAIAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAADAAAABgcAAAEAAAAAAAAADwcAAAIAAAAAAAAAAgAAAA4IAAABAAAAAAAAABMIAAABAAAAAAAAAAAAAAAADwAAAAoAAAAAAAAAAAAAHAAAAAMAAAAAAAAAAAAAAP//+BGHAAAAAAAAAAwA6wIABAAEyAEAACIAIBduDwEAAQCMAGAACxdfDwAAAAAAAAAAAAAAAAAAAAAKACwDAAQABF8BNQIABAAE4wGOAQAEAASbAukFOQc4B3QCsQaYC5YLcAVTAxcIFgg9BQMAAAAGAQkBAwEAAMwFPQw6DMAGowLRANEAHgBeAI4CjwIdBGEFJwkmCaQBqQUzCzELCQaDAswAzAAbAF0ARAJFAqEDPwVuCG4IfAGvBwAEAATLBvgSrwcABAAEywb4Eq8HAAQABMsG+BKvBwAEAATLBvgSAAQABAAEAARQEQAEAAQABAAEUBGvBwAEAATLBvgSrwcABAAEywb4Eq8HAAQABMsG+BKvBwAEAATLBvgSrwcABAAEywb4Eq0HAQT+A8kG+BIAAAAAAAAAAAAA+AcABAAEdgZQFCoJAAQABIEFWBuSCAAEAAT0BXAXwQUABAAEqQmADPoGAAQABD8Jgg55BwAEAAQZB/kR4wgABAAE2gWwGPgHAAQABHYGRRT4BwAEAAR2BkUU+AcABAAEdgZFFPgHAAQABHYGRRT4BwAEAAR2BkUU/wMABAAEEgT2EP8DAAQABBIE9hD/AwAEAAQSBPYQ/wMABAAEEgT2EP8DAAQABBIE9hDQ/oQBcAOUKuL+jAFZAxAnEP+jASADbCBE/78B6AJYG3n/3gGwAnAXlP/uAZcC4BWy/wICegJQFOL/HQJLAlwSGgBEAhwCaBBQAG4C8wHYDoAAlgLPAawNuQDIAqgBgAzoAO8ChQG4CxEBHwNyAfAKewGgAz4BYAn0AREIIQgACAAIAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAANABgAagAXABUAIQAKADYBUAB3ADUAHwAUAFMAEQAaAB0AlAGQBckBuQGKApQAXQVSAeEB7gBzAE8AgAEqAAgADAA0AisFMwPtAnkC5wAMA7MAAQF3ACoAHABeAAMAhwDLAB0Dhit/D/YLGgpLA4gOJgWtCgcHVgHHAHQCPAAAAAAAAAAAgAAAAAQABAAEYAoLDyEZuw9+/8n/uA9IEIYAOQBMEAAAAAEFAGv7BwB88QcAN/QDAIiBAAQABAAEAAAAAAAA/f8AAP8fAAEAAAAAAASuAqsB6wF+ApYBGAMAAAAAAAAAAAAAHwA/AF8AfwCfAL8A3wD/AAAAIQBGAGoAigCmAMEA3gD/AAEAAABwAAAAEAAgAEAAYACAAMAAAADh/+T/4f/k/+L/4/8sBDAELwQxBC4EMQQvBIgEAQD/B/8HAAgACPY49joQJwAAAAAAAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgBlANIAAAEAAQABAAEAAQAACgBzANIAAAEAAQABAAEAAWUAZQBzAAEAAQDzAPMACgBGAEoAZABnAJMAmwC+AMcA5ADrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEkAYgBmAG0AmQC8AMYA4gDnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG8FAARbAwAAAAAAAAAAQwBRAKp96gEAAAAAAAAAAGECrAAAAEsCYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAgAEAARbAmQAAABDAFMAtX3PAQAAAAAAAAAAZAB8AGsAgwArAPsB/wAiWuhhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQBHAG0AjACnAMEA3gD/AAAAAAAAAAAAAAAAAAAAAAAAAGQAAABzAAEA8wAAAAAAAAAAAAAAAAAAACEARgBqAIoApgDBAN4A/wA1AD0ASABOAFIAVgBbAGIAaQBwAHYAewCFAI8AkgCUAJcAmACXAJMAiwCDAHwAdwB1AHQAdAB4AH0AhgCKAJAAlwCZAJ4AogCqALAAsACxAKgAoACUAIcAegBkAFgASgBDAD0AOAA2ADQAMQAvAC0AKQAmACUAJgAjAEAAOwAAAP8AAgB8AGsAIlroYQAAAAAAAAAAAAD7Af8AKwBgAp0AAABNAmQAAABgAQAAAACDAAAAAAALAAAAPAAAAP//AAD///////8AAAAAAABBAAAAOAAAAAAAZABkAGQAZAAKAGQAZABkAAAAMgBkAGQAGwABAAAAAAA8AAAAfABrACJa6GEAAAAAAAAAAAAA+wH/ACsAtX3PAQAAAAAAAAAAIlroYQAAfAAAAAAAAAAAAAAAPAAAAAAAIQBGAGoAigCmAMEA3gD/AAAAAAAVAgAEAARbAoEAgQCBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAAAAAAAAAAAAKAAAA/////wAAAAAKAAAAAAAAAAoAAACw////CgAAAAAhyAEAAAAAAAAAAAAAAAAAAAAAahiAFgAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABwAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAJAAAEGiHACuMAInoABa8AAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAEAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAACAIJADQApqoABjwH0NDQAEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJSSoAjgMAAA=="
+        },
+        "value_string": "MakerNote\u003cTYPE-ID=[28 00 01 00 03 00 31 00 00 00 74 05 00 00 02 00 03 00 04 00] LEN=(8152) SHA1=[d4154aa7df5474efe7ab38de2595919b9b4cc29f]\u003e"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 37510,
+        "tag_name": "UserComment",
+        "tag_type_id": 7,
+        "tag_type_name": "UNDEFINED",
+        "unit_count": 264,
+        "value": {
+            "EncodingType": 3,
+            "EncodingBytes": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
+        },
+        "value_string": "UserComment\u003cSIZE=(256) ENCODING=[UNDEFINED] V=[0 0 0 0 0 0 0 0]... LEN=(256)\u003e"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 37520,
+        "tag_name": "SubSecTime",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 3,
+        "value": "00",
+        "value_string": "00"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 37521,
+        "tag_name": "SubSecTimeOriginal",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 3,
+        "value": "00",
+        "value_string": "00"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 37522,
+        "tag_name": "SubSecTimeDigitized",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 3,
+        "value": "00",
+        "value_string": "00"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 40960,
+        "tag_name": "FlashpixVersion",
+        "tag_type_id": 7,
+        "tag_type_name": "UNDEFINED",
+        "unit_count": 4,
+        "value": "0100",
+        "value_string": "0100"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 40961,
+        "tag_name": "ColorSpace",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "1",
+        "value_string": "1"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 40962,
+        "tag_name": "PixelXDimension",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "3840",
+        "value_string": "3840"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 40963,
+        "tag_name": "PixelYDimension",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "2560",
+        "value_string": "2560"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 40965,
+        "tag_name": "InteroperabilityTag",
+        "tag_type_id": 4,
+        "tag_type_name": "LONG",
+        "unit_count": 1,
+        "value": "9326",
+        "value_string": "9326"
+    },
+    {
+        "ifd_path": "IFD/Exif/Iop",
+        "fq_ifd_path": "IFD/Exif/Iop",
+        "ifd_index": 0,
+        "tag_id": 1,
+        "tag_name": "InteroperabilityIndex",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 4,
+        "value": "R98",
+        "value_string": "R98"
+    },
+    {
+        "ifd_path": "IFD/Exif/Iop",
+        "fq_ifd_path": "IFD/Exif/Iop",
+        "ifd_index": 0,
+        "tag_id": 2,
+        "tag_name": "InteroperabilityVersion",
+        "tag_type_id": 7,
+        "tag_type_name": "UNDEFINED",
+        "unit_count": 4,
+        "value": "0100",
+        "value_string": "0100"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 41486,
+        "tag_name": "FocalPlaneXResolution",
+        "tag_type_id": 5,
+        "tag_type_name": "RATIONAL",
+        "unit_count": 1,
+        "value": "3840000/1461",
+        "value_string": "3840000/1461"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 41487,
+        "tag_name": "FocalPlaneYResolution",
+        "tag_type_id": 5,
+        "tag_type_name": "RATIONAL",
+        "unit_count": 1,
+        "value": "2560000/972",
+        "value_string": "2560000/972"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 41488,
+        "tag_name": "FocalPlaneResolutionUnit",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "2",
+        "value_string": "2"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 41985,
+        "tag_name": "CustomRendered",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "0",
+        "value_string": "0"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 41986,
+        "tag_name": "ExposureMode",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "0",
+        "value_string": "0"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 41987,
+        "tag_name": "WhiteBalance",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "0",
+        "value_string": "0"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 41990,
+        "tag_name": "SceneCaptureType",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "0",
+        "value_string": "0"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 42032,
+        "tag_name": "CameraOwnerName",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 1,
+        "value": "",
+        "value_string": ""
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 42033,
+        "tag_name": "BodySerialNumber",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 13,
+        "value": "063024020097",
+        "value_string": "063024020097"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 42034,
+        "tag_name": "LensSpecification",
+        "tag_type_id": 5,
+        "tag_type_name": "RATIONAL",
+        "unit_count": 4,
+        "value": "16/1...",
+        "value_string": "16/1..."
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 42036,
+        "tag_name": "LensModel",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 22,
+        "value": "EF16-35mm f/4L IS USM",
+        "value_string": "EF16-35mm f/4L IS USM"
+    },
+    {
+        "ifd_path": "IFD/Exif",
+        "fq_ifd_path": "IFD/Exif",
+        "ifd_index": 0,
+        "tag_id": 42037,
+        "tag_name": "LensSerialNumber",
+        "tag_type_id": 2,
+        "tag_type_name": "ASCII",
+        "unit_count": 11,
+        "value": "2400001068",
+        "value_string": "2400001068"
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 0,
+        "tag_id": 34853,
+        "tag_name": "GPSTag",
+        "tag_type_id": 4,
+        "tag_type_name": "LONG",
+        "unit_count": 1,
+        "value": "9554",
+        "value_string": "9554"
+    },
+    {
+        "ifd_path": "IFD/GPSInfo",
+        "fq_ifd_path": "IFD/GPSInfo",
+        "ifd_index": 0,
+        "tag_id": 0,
+        "tag_name": "GPSVersionID",
+        "tag_type_id": 1,
+        "tag_type_name": "BYTE",
+        "unit_count": 4,
+        "value": "02 03 00 00",
+        "value_string": "02 03 00 00"
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 1,
+        "tag_id": 259,
+        "tag_name": "Compression",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "6",
+        "value_string": "6"
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 1,
+        "tag_id": 282,
+        "tag_name": "XResolution",
+        "tag_type_id": 5,
+        "tag_type_name": "RATIONAL",
+        "unit_count": 1,
+        "value": "72/1",
+        "value_string": "72/1"
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 1,
+        "tag_id": 283,
+        "tag_name": "YResolution",
+        "tag_type_id": 5,
+        "tag_type_name": "RATIONAL",
+        "unit_count": 1,
+        "value": "72/1",
+        "value_string": "72/1"
+    },
+    {
+        "ifd_path": "IFD",
+        "fq_ifd_path": "IFD",
+        "ifd_index": 1,
+        "tag_id": 296,
+        "tag_name": "ResolutionUnit",
+        "tag_type_id": 3,
+        "tag_type_name": "SHORT",
+        "unit_count": 1,
+        "value": "2",
+        "value_string": "2"
+    }
+]
diff --git a/v2/assets/gps.jpg b/v2/assets/gps.jpg
new file mode 100644
index 0000000..fbb680a
Binary files /dev/null and b/v2/assets/gps.jpg differ
diff --git a/v2/assets/raw_tags/requirements.txt b/v2/assets/raw_tags/requirements.txt
new file mode 100644
index 0000000..58d3608
--- /dev/null
+++ b/v2/assets/raw_tags/requirements.txt
@@ -0,0 +1 @@
+ruamel.yaml
diff --git a/v2/assets/raw_tags/tags.html b/v2/assets/raw_tags/tags.html
new file mode 100644
index 0000000..85cd836
--- /dev/null
+++ b/v2/assets/raw_tags/tags.html
@@ -0,0 +1,2492 @@
+<table class="table table-striped">
+<thead><tr>
+<th>Tag (hex)</th>
+<th>Tag (dec)</th>
+<th>IFD</th>
+<th>Key</th>
+<th>Type</th>
+<th>Tag description</th>
+</tr></thead>
+<tbody>
+<tr>
+<td>0x000b</td>
+<td>11</td>
+<td>Image</td>
+<td>Exif.Image.ProcessingSoftware</td>
+<td>Ascii</td>
+<td>The name and version of the software used to post-process the picture.</td>
+</tr>
+<tr>
+<td>0x00fe</td>
+<td>254</td>
+<td>Image</td>
+<td>Exif.Image.NewSubfileType</td>
+<td>Long</td>
+<td>A general indication of the kind of data contained in this subfile.</td>
+</tr>
+<tr>
+<td>0x00ff</td>
+<td>255</td>
+<td>Image</td>
+<td>Exif.Image.SubfileType</td>
+<td>Short</td>
+<td>A general indication of the kind of data contained in this subfile. This field is deprecated. The NewSubfileType field should be used instead.</td>
+</tr>
+<tr>
+<td>0x0100</td>
+<td>256</td>
+<td>Image</td>
+<td>Exif.Image.ImageWidth</td>
+<td>Long</td>
+<td>The number of columns of image data, equal to the number of pixels per row. In JPEG compressed data a JPEG marker is used instead of this tag.</td>
+</tr>
+<tr>
+<td>0x0101</td>
+<td>257</td>
+<td>Image</td>
+<td>Exif.Image.ImageLength</td>
+<td>Long</td>
+<td>The number of rows of image data. In JPEG compressed data a JPEG marker is used instead of this tag.</td>
+</tr>
+<tr>
+<td>0x0102</td>
+<td>258</td>
+<td>Image</td>
+<td>Exif.Image.BitsPerSample</td>
+<td>Short</td>
+<td>The number of bits per image component. In this standard each component of the image is 8 bits, so the value for this tag is 8. See also &lt;SamplesPerPixel&gt;. In JPEG compressed data a JPEG marker is used instead of this tag.</td>
+</tr>
+<tr>
+<td>0x0103</td>
+<td>259</td>
+<td>Image</td>
+<td>Exif.Image.Compression</td>
+<td>Short</td>
+<td>The compression scheme used for the image data. When a primary image is JPEG compressed, this designation is not necessary and is omitted. When thumbnails use JPEG compression, this tag value is set to 6.</td>
+</tr>
+<tr>
+<td>0x0106</td>
+<td>262</td>
+<td>Image</td>
+<td>Exif.Image.PhotometricInterpretation</td>
+<td>Short</td>
+<td>The pixel composition. In JPEG compressed data a JPEG marker is used instead of this tag.</td>
+</tr>
+<tr>
+<td>0x0107</td>
+<td>263</td>
+<td>Image</td>
+<td>Exif.Image.Thresholding</td>
+<td>Short</td>
+<td>For black and white TIFF files that represent shades of gray, the technique used to convert from gray to black and white pixels.</td>
+</tr>
+<tr>
+<td>0x0108</td>
+<td>264</td>
+<td>Image</td>
+<td>Exif.Image.CellWidth</td>
+<td>Short</td>
+<td>The width of the dithering or halftoning matrix used to create a dithered or halftoned bilevel file.</td>
+</tr>
+<tr>
+<td>0x0109</td>
+<td>265</td>
+<td>Image</td>
+<td>Exif.Image.CellLength</td>
+<td>Short</td>
+<td>The length of the dithering or halftoning matrix used to create a dithered or halftoned bilevel file.</td>
+</tr>
+<tr>
+<td>0x010a</td>
+<td>266</td>
+<td>Image</td>
+<td>Exif.Image.FillOrder</td>
+<td>Short</td>
+<td>The logical order of bits within a byte</td>
+</tr>
+<tr>
+<td>0x010d</td>
+<td>269</td>
+<td>Image</td>
+<td>Exif.Image.DocumentName</td>
+<td>Ascii</td>
+<td>The name of the document from which this image was scanned</td>
+</tr>
+<tr>
+<td>0x010e</td>
+<td>270</td>
+<td>Image</td>
+<td>Exif.Image.ImageDescription</td>
+<td>Ascii</td>
+<td>A character string giving the title of the image. It may be a comment such as "1988 company picnic" or the like. Two-bytes character codes cannot be used. When a 2-bytes code is necessary, the Exif Private tag &lt;UserComment&gt; is to be used.</td>
+</tr>
+<tr>
+<td>0x010f</td>
+<td>271</td>
+<td>Image</td>
+<td>Exif.Image.Make</td>
+<td>Ascii</td>
+<td>The manufacturer of the recording equipment. This is the manufacturer of the DSC, scanner, video digitizer or other equipment that generated the image. When the field is left blank, it is treated as unknown.</td>
+</tr>
+<tr>
+<td>0x0110</td>
+<td>272</td>
+<td>Image</td>
+<td>Exif.Image.Model</td>
+<td>Ascii</td>
+<td>The model name or model number of the equipment. This is the model name or number of the DSC, scanner, video digitizer or other equipment that generated the image. When the field is left blank, it is treated as unknown.</td>
+</tr>
+<tr>
+<td>0x0111</td>
+<td>273</td>
+<td>Image</td>
+<td>Exif.Image.StripOffsets</td>
+<td>Long</td>
+<td>For each strip, the byte offset of that strip. It is recommended that this be selected so the number of strip bytes does not exceed 64 Kbytes. With JPEG compressed data this designation is not needed and is omitted. See also &lt;RowsPerStrip&gt; and &lt;StripByteCounts&gt;.</td>
+</tr>
+<tr>
+<td>0x0112</td>
+<td>274</td>
+<td>Image</td>
+<td>Exif.Image.Orientation</td>
+<td>Short</td>
+<td>The image orientation viewed in terms of rows and columns.</td>
+</tr>
+<tr>
+<td>0x0115</td>
+<td>277</td>
+<td>Image</td>
+<td>Exif.Image.SamplesPerPixel</td>
+<td>Short</td>
+<td>The number of components per pixel. Since this standard applies to RGB and YCbCr images, the value set for this tag is 3. In JPEG compressed data a JPEG marker is used instead of this tag.</td>
+</tr>
+<tr>
+<td>0x0116</td>
+<td>278</td>
+<td>Image</td>
+<td>Exif.Image.RowsPerStrip</td>
+<td>Long</td>
+<td>The number of rows per strip. This is the number of rows in the image of one strip when an image is divided into strips. With JPEG compressed data this designation is not needed and is omitted. See also &lt;StripOffsets&gt; and &lt;StripByteCounts&gt;.</td>
+</tr>
+<tr>
+<td>0x0117</td>
+<td>279</td>
+<td>Image</td>
+<td>Exif.Image.StripByteCounts</td>
+<td>Long</td>
+<td>The total number of bytes in each strip. With JPEG compressed data this designation is not needed and is omitted.</td>
+</tr>
+<tr>
+<td>0x011a</td>
+<td>282</td>
+<td>Image</td>
+<td>Exif.Image.XResolution</td>
+<td>Rational</td>
+<td>The number of pixels per &lt;ResolutionUnit&gt; in the &lt;ImageWidth&gt; direction. When the image resolution is unknown, 72 [dpi] is designated.</td>
+</tr>
+<tr>
+<td>0x011b</td>
+<td>283</td>
+<td>Image</td>
+<td>Exif.Image.YResolution</td>
+<td>Rational</td>
+<td>The number of pixels per &lt;ResolutionUnit&gt; in the &lt;ImageLength&gt; direction. The same value as &lt;XResolution&gt; is designated.</td>
+</tr>
+<tr>
+<td>0x011c</td>
+<td>284</td>
+<td>Image</td>
+<td>Exif.Image.PlanarConfiguration</td>
+<td>Short</td>
+<td>Indicates whether pixel components are recorded in a chunky or planar format. In JPEG compressed files a JPEG marker is used instead of this tag. If this field does not exist, the TIFF default of 1 (chunky) is assumed.</td>
+</tr>
+<tr>
+<td>0x0122</td>
+<td>290</td>
+<td>Image</td>
+<td>Exif.Image.GrayResponseUnit</td>
+<td>Short</td>
+<td>The precision of the information contained in the GrayResponseCurve.</td>
+</tr>
+<tr>
+<td>0x0123</td>
+<td>291</td>
+<td>Image</td>
+<td>Exif.Image.GrayResponseCurve</td>
+<td>Short</td>
+<td>For grayscale data, the optical density of each possible pixel value.</td>
+</tr>
+<tr>
+<td>0x0124</td>
+<td>292</td>
+<td>Image</td>
+<td>Exif.Image.T4Options</td>
+<td>Long</td>
+<td>T.4-encoding options.</td>
+</tr>
+<tr>
+<td>0x0125</td>
+<td>293</td>
+<td>Image</td>
+<td>Exif.Image.T6Options</td>
+<td>Long</td>
+<td>T.6-encoding options.</td>
+</tr>
+<tr>
+<td>0x0128</td>
+<td>296</td>
+<td>Image</td>
+<td>Exif.Image.ResolutionUnit</td>
+<td>Short</td>
+<td>The unit for measuring &lt;XResolution&gt; and &lt;YResolution&gt;. The same unit is used for both &lt;XResolution&gt; and &lt;YResolution&gt;. If the image resolution is unknown, 2 (inches) is designated.</td>
+</tr>
+<tr>
+<td>0x0129</td>
+<td>297</td>
+<td>Image</td>
+<td>Exif.Image.PageNumber</td>
+<td>Short</td>
+<td>The page number of the page from which this image was scanned.</td>
+</tr>
+<tr>
+<td>0x012d</td>
+<td>301</td>
+<td>Image</td>
+<td>Exif.Image.TransferFunction</td>
+<td>Short</td>
+<td>A transfer function for the image, described in tabular style. Normally this tag is not necessary, since color space is specified in the color space information tag (&lt;ColorSpace&gt;).</td>
+</tr>
+<tr>
+<td>0x0131</td>
+<td>305</td>
+<td>Image</td>
+<td>Exif.Image.Software</td>
+<td>Ascii</td>
+<td>This tag records the name and version of the software or firmware of the camera or image input device used to generate the image. The detailed format is not specified, but it is recommended that the example shown below be followed. When the field is left blank, it is treated as unknown.</td>
+</tr>
+<tr>
+<td>0x0132</td>
+<td>306</td>
+<td>Image</td>
+<td>Exif.Image.DateTime</td>
+<td>Ascii</td>
+<td>The date and time of image creation. In Exif standard, it is the date and time the file was changed.</td>
+</tr>
+<tr>
+<td>0x013b</td>
+<td>315</td>
+<td>Image</td>
+<td>Exif.Image.Artist</td>
+<td>Ascii</td>
+<td>This tag records the name of the camera owner, photographer or image creator. The detailed format is not specified, but it is recommended that the information be written as in the example below for ease of Interoperability. When the field is left blank, it is treated as unknown. Ex.) "Camera owner, John Smith; Photographer, Michael Brown; Image creator, Ken James"</td>
+</tr>
+<tr>
+<td>0x013c</td>
+<td>316</td>
+<td>Image</td>
+<td>Exif.Image.HostComputer</td>
+<td>Ascii</td>
+<td>This tag records information about the host computer used to generate the image.</td>
+</tr>
+<tr>
+<td>0x013d</td>
+<td>317</td>
+<td>Image</td>
+<td>Exif.Image.Predictor</td>
+<td>Short</td>
+<td>A predictor is a mathematical operator that is applied to the image data before an encoding scheme is applied.</td>
+</tr>
+<tr>
+<td>0x013e</td>
+<td>318</td>
+<td>Image</td>
+<td>Exif.Image.WhitePoint</td>
+<td>Rational</td>
+<td>The chromaticity of the white point of the image. Normally this tag is not necessary, since color space is specified in the colorspace information tag (&lt;ColorSpace&gt;).</td>
+</tr>
+<tr>
+<td>0x013f</td>
+<td>319</td>
+<td>Image</td>
+<td>Exif.Image.PrimaryChromaticities</td>
+<td>Rational</td>
+<td>The chromaticity of the three primary colors of the image. Normally this tag is not necessary, since colorspace is specified in the colorspace information tag (&lt;ColorSpace&gt;).</td>
+</tr>
+<tr>
+<td>0x0140</td>
+<td>320</td>
+<td>Image</td>
+<td>Exif.Image.ColorMap</td>
+<td>Short</td>
+<td>A color map for palette color images. This field defines a Red-Green-Blue color map (often called a lookup table) for palette-color images. In a palette-color image, a pixel value is used to index into an RGB lookup table.</td>
+</tr>
+<tr>
+<td>0x0141</td>
+<td>321</td>
+<td>Image</td>
+<td>Exif.Image.HalftoneHints</td>
+<td>Short</td>
+<td>The purpose of the HalftoneHints field is to convey to the halftone function the range of gray levels within a colorimetrically-specified image that should retain tonal detail.</td>
+</tr>
+<tr>
+<td>0x0142</td>
+<td>322</td>
+<td>Image</td>
+<td>Exif.Image.TileWidth</td>
+<td>Short</td>
+<td>The tile width in pixels. This is the number of columns in each tile.</td>
+</tr>
+<tr>
+<td>0x0143</td>
+<td>323</td>
+<td>Image</td>
+<td>Exif.Image.TileLength</td>
+<td>Short</td>
+<td>The tile length (height) in pixels. This is the number of rows in each tile.</td>
+</tr>
+<tr>
+<td>0x0144</td>
+<td>324</td>
+<td>Image</td>
+<td>Exif.Image.TileOffsets</td>
+<td>Short</td>
+<td>For each tile, the byte offset of that tile, as compressed and stored on disk. The offset is specified with respect to the beginning of the TIFF file. Note that this implies that each tile has a location independent of the locations of other tiles.</td>
+</tr>
+<tr>
+<td>0x0145</td>
+<td>325</td>
+<td>Image</td>
+<td>Exif.Image.TileByteCounts</td>
+<td>Short</td>
+<td>For each tile, the number of (compressed) bytes in that tile. See TileOffsets for a description of how the byte counts are ordered.</td>
+</tr>
+<tr>
+<td>0x014a</td>
+<td>330</td>
+<td>Image</td>
+<td>Exif.Image.SubIFDs</td>
+<td>Long</td>
+<td>Defined by Adobe Corporation to enable TIFF Trees within a TIFF file.</td>
+</tr>
+<tr>
+<td>0x014c</td>
+<td>332</td>
+<td>Image</td>
+<td>Exif.Image.InkSet</td>
+<td>Short</td>
+<td>The set of inks used in a separated (PhotometricInterpretation=5) image.</td>
+</tr>
+<tr>
+<td>0x014d</td>
+<td>333</td>
+<td>Image</td>
+<td>Exif.Image.InkNames</td>
+<td>Ascii</td>
+<td>The name of each ink used in a separated (PhotometricInterpretation=5) image.</td>
+</tr>
+<tr>
+<td>0x014e</td>
+<td>334</td>
+<td>Image</td>
+<td>Exif.Image.NumberOfInks</td>
+<td>Short</td>
+<td>The number of inks. Usually equal to SamplesPerPixel, unless there are extra samples.</td>
+</tr>
+<tr>
+<td>0x0150</td>
+<td>336</td>
+<td>Image</td>
+<td>Exif.Image.DotRange</td>
+<td>Byte</td>
+<td>The component values that correspond to a 0% dot and 100% dot.</td>
+</tr>
+<tr>
+<td>0x0151</td>
+<td>337</td>
+<td>Image</td>
+<td>Exif.Image.TargetPrinter</td>
+<td>Ascii</td>
+<td>A description of the printing environment for which this separation is intended.</td>
+</tr>
+<tr>
+<td>0x0152</td>
+<td>338</td>
+<td>Image</td>
+<td>Exif.Image.ExtraSamples</td>
+<td>Short</td>
+<td>Specifies that each pixel has m extra components whose interpretation is defined by one of the values listed below.</td>
+</tr>
+<tr>
+<td>0x0153</td>
+<td>339</td>
+<td>Image</td>
+<td>Exif.Image.SampleFormat</td>
+<td>Short</td>
+<td>This field specifies how to interpret each data sample in a pixel.</td>
+</tr>
+<tr>
+<td>0x0154</td>
+<td>340</td>
+<td>Image</td>
+<td>Exif.Image.SMinSampleValue</td>
+<td>Short</td>
+<td>This field specifies the minimum sample value.</td>
+</tr>
+<tr>
+<td>0x0155</td>
+<td>341</td>
+<td>Image</td>
+<td>Exif.Image.SMaxSampleValue</td>
+<td>Short</td>
+<td>This field specifies the maximum sample value.</td>
+</tr>
+<tr>
+<td>0x0156</td>
+<td>342</td>
+<td>Image</td>
+<td>Exif.Image.TransferRange</td>
+<td>Short</td>
+<td>Expands the range of the TransferFunction</td>
+</tr>
+<tr>
+<td>0x0157</td>
+<td>343</td>
+<td>Image</td>
+<td>Exif.Image.ClipPath</td>
+<td>Byte</td>
+<td>A TIFF ClipPath is intended to mirror the essentials of PostScript's path creation functionality.</td>
+</tr>
+<tr>
+<td>0x0158</td>
+<td>344</td>
+<td>Image</td>
+<td>Exif.Image.XClipPathUnits</td>
+<td>SShort</td>
+<td>The number of units that span the width of the image, in terms of integer ClipPath coordinates.</td>
+</tr>
+<tr>
+<td>0x0159</td>
+<td>345</td>
+<td>Image</td>
+<td>Exif.Image.YClipPathUnits</td>
+<td>SShort</td>
+<td>The number of units that span the height of the image, in terms of integer ClipPath coordinates.</td>
+</tr>
+<tr>
+<td>0x015a</td>
+<td>346</td>
+<td>Image</td>
+<td>Exif.Image.Indexed</td>
+<td>Short</td>
+<td>Indexed images are images where the 'pixels' do not represent color values, but rather an index (usually 8-bit) into a separate color table, the ColorMap.</td>
+</tr>
+<tr>
+<td>0x015b</td>
+<td>347</td>
+<td>Image</td>
+<td>Exif.Image.JPEGTables</td>
+<td>Undefined</td>
+<td>This optional tag may be used to encode the JPEG quantization and Huffman tables for subsequent use by the JPEG decompression process.</td>
+</tr>
+<tr>
+<td>0x015f</td>
+<td>351</td>
+<td>Image</td>
+<td>Exif.Image.OPIProxy</td>
+<td>Short</td>
+<td>OPIProxy gives information concerning whether this image is a low-resolution proxy of a high-resolution image (Adobe OPI).</td>
+</tr>
+<tr>
+<td>0x0200</td>
+<td>512</td>
+<td>Image</td>
+<td>Exif.Image.JPEGProc</td>
+<td>Long</td>
+<td>This field indicates the process used to produce the compressed data</td>
+</tr>
+<tr>
+<td>0x0201</td>
+<td>513</td>
+<td>Image</td>
+<td>Exif.Image.JPEGInterchangeFormat</td>
+<td>Long</td>
+<td>The offset to the start byte (SOI) of JPEG compressed thumbnail data. This is not used for primary image JPEG data.</td>
+</tr>
+<tr>
+<td>0x0202</td>
+<td>514</td>
+<td>Image</td>
+<td>Exif.Image.JPEGInterchangeFormatLength</td>
+<td>Long</td>
+<td>The number of bytes of JPEG compressed thumbnail data. This is not used for primary image JPEG data. JPEG thumbnails are not divided but are recorded as a continuous JPEG bitstream from SOI to EOI. Appn and COM markers should not be recorded. Compressed thumbnails must be recorded in no more than 64 Kbytes, including all other data to be recorded in APP1.</td>
+</tr>
+<tr>
+<td>0x0203</td>
+<td>515</td>
+<td>Image</td>
+<td>Exif.Image.JPEGRestartInterval</td>
+<td>Short</td>
+<td>This Field indicates the length of the restart interval used in the compressed image data.</td>
+</tr>
+<tr>
+<td>0x0205</td>
+<td>517</td>
+<td>Image</td>
+<td>Exif.Image.JPEGLosslessPredictors</td>
+<td>Short</td>
+<td>This Field points to a list of lossless predictor-selection values, one per component.</td>
+</tr>
+<tr>
+<td>0x0206</td>
+<td>518</td>
+<td>Image</td>
+<td>Exif.Image.JPEGPointTransforms</td>
+<td>Short</td>
+<td>This Field points to a list of point transform values, one per component.</td>
+</tr>
+<tr>
+<td>0x0207</td>
+<td>519</td>
+<td>Image</td>
+<td>Exif.Image.JPEGQTables</td>
+<td>Long</td>
+<td>This Field points to a list of offsets to the quantization tables, one per component.</td>
+</tr>
+<tr>
+<td>0x0208</td>
+<td>520</td>
+<td>Image</td>
+<td>Exif.Image.JPEGDCTables</td>
+<td>Long</td>
+<td>This Field points to a list of offsets to the DC Huffman tables or the lossless Huffman tables, one per component.</td>
+</tr>
+<tr>
+<td>0x0209</td>
+<td>521</td>
+<td>Image</td>
+<td>Exif.Image.JPEGACTables</td>
+<td>Long</td>
+<td>This Field points to a list of offsets to the Huffman AC tables, one per component.</td>
+</tr>
+<tr>
+<td>0x0211</td>
+<td>529</td>
+<td>Image</td>
+<td>Exif.Image.YCbCrCoefficients</td>
+<td>Rational</td>
+<td>The matrix coefficients for transformation from RGB to YCbCr image data. No default is given in TIFF; but here the value given in Appendix E, "Color Space Guidelines", is used as the default. The color space is declared in a color space information tag, with the default being the value that gives the optimal image characteristics Interoperability this condition.</td>
+</tr>
+<tr>
+<td>0x0212</td>
+<td>530</td>
+<td>Image</td>
+<td>Exif.Image.YCbCrSubSampling</td>
+<td>Short</td>
+<td>The sampling ratio of chrominance components in relation to the luminance component. In JPEG compressed data a JPEG marker is used instead of this tag.</td>
+</tr>
+<tr>
+<td>0x0213</td>
+<td>531</td>
+<td>Image</td>
+<td>Exif.Image.YCbCrPositioning</td>
+<td>Short</td>
+<td>The position of chrominance components in relation to the luminance component. This field is designated only for JPEG compressed data or uncompressed YCbCr data. The TIFF default is 1 (centered); but when Y:Cb:Cr = 4:2:2 it is recommended in this standard that 2 (co-sited) be used to record data, in order to improve the image quality when viewed on TV systems. When this field does not exist, the reader shall assume the TIFF default. In the case of Y:Cb:Cr = 4:2:0, the TIFF default (centered) is recommended. If the reader does not have the capability of supporting both kinds of &lt;YCbCrPositioning&gt;, it shall follow the TIFF default regardless of the value in this field. It is preferable that readers be able to support both centered and co-sited positioning.</td>
+</tr>
+<tr>
+<td>0x0214</td>
+<td>532</td>
+<td>Image</td>
+<td>Exif.Image.ReferenceBlackWhite</td>
+<td>Rational</td>
+<td>The reference black point value and reference white point value. No defaults are given in TIFF, but the values below are given as defaults here. The color space is declared in a color space information tag, with the default being the value that gives the optimal image characteristics Interoperability these conditions.</td>
+</tr>
+<tr>
+<td>0x02bc</td>
+<td>700</td>
+<td>Image</td>
+<td>Exif.Image.XMLPacket</td>
+<td>Byte</td>
+<td>XMP Metadata (Adobe technote 9-14-02)</td>
+</tr>
+<tr>
+<td>0x4746</td>
+<td>18246</td>
+<td>Image</td>
+<td>Exif.Image.Rating</td>
+<td>Short</td>
+<td>Rating tag used by Windows</td>
+</tr>
+<tr>
+<td>0x4749</td>
+<td>18249</td>
+<td>Image</td>
+<td>Exif.Image.RatingPercent</td>
+<td>Short</td>
+<td>Rating tag used by Windows, value in percent</td>
+</tr>
+<tr>
+<td>0x800d</td>
+<td>32781</td>
+<td>Image</td>
+<td>Exif.Image.ImageID</td>
+<td>Ascii</td>
+<td>ImageID is the full pathname of the original, high-resolution image, or any other identifying string that uniquely identifies the original image (Adobe OPI).</td>
+</tr>
+<tr>
+<td>0x828d</td>
+<td>33421</td>
+<td>Image</td>
+<td>Exif.Image.CFARepeatPatternDim</td>
+<td>Short</td>
+<td>Contains two values representing the minimum rows and columns to define the repeating patterns of the color filter array</td>
+</tr>
+<tr>
+<td>0x828e</td>
+<td>33422</td>
+<td>Image</td>
+<td>Exif.Image.CFAPattern</td>
+<td>Byte</td>
+<td>Indicates the color filter array (CFA) geometric pattern of the image sensor when a one-chip color area sensor is used. It does not apply to all sensing methods</td>
+</tr>
+<tr>
+<td>0x828f</td>
+<td>33423</td>
+<td>Image</td>
+<td>Exif.Image.BatteryLevel</td>
+<td>Rational</td>
+<td>Contains a value of the battery level as a fraction or string</td>
+</tr>
+<tr>
+<td>0x8298</td>
+<td>33432</td>
+<td>Image</td>
+<td>Exif.Image.Copyright</td>
+<td>Ascii</td>
+<td>Copyright information. In this standard the tag is used to indicate both the photographer and editor copyrights. It is the copyright notice of the person or organization claiming rights to the image. The Interoperability copyright statement including date and rights should be written in this field; e.g., "Copyright, John Smith, 19xx. All rights reserved.". In this standard the field records both the photographer and editor copyrights, with each recorded in a separate part of the statement. When there is a clear distinction between the photographer and editor copyrights, these are to be written in the order of photographer followed by editor copyright, separated by NULL (in this case since the statement also ends with a NULL, there are two NULL codes). When only the photographer copyright is given, it is terminated by one NULL code. When only the editor copyright is given, the photographer copyright part consists of one space followed by a terminating NULL code, then the editor copyright is given. When the field is left blank, it is treated as unknown.</td>
+</tr>
+<tr>
+<td>0x829a</td>
+<td>33434</td>
+<td>Image</td>
+<td>Exif.Image.ExposureTime</td>
+<td>Rational</td>
+<td>Exposure time, given in seconds.</td>
+</tr>
+<tr>
+<td>0x829d</td>
+<td>33437</td>
+<td>Image</td>
+<td>Exif.Image.FNumber</td>
+<td>Rational</td>
+<td>The F number.</td>
+</tr>
+<tr>
+<td>0x83bb</td>
+<td>33723</td>
+<td>Image</td>
+<td>Exif.Image.IPTCNAA</td>
+<td>Long</td>
+<td>Contains an IPTC/NAA record</td>
+</tr>
+<tr>
+<td>0x8649</td>
+<td>34377</td>
+<td>Image</td>
+<td>Exif.Image.ImageResources</td>
+<td>Byte</td>
+<td>Contains information embedded by the Adobe Photoshop application</td>
+</tr>
+<tr>
+<td>0x8769</td>
+<td>34665</td>
+<td>Image</td>
+<td>Exif.Image.ExifTag</td>
+<td>Long</td>
+<td>A pointer to the Exif IFD. Interoperability, Exif IFD has the same structure as that of the IFD specified in TIFF. ordinarily, however, it does not contain image data as in the case of TIFF.</td>
+</tr>
+<tr>
+<td>0x8773</td>
+<td>34675</td>
+<td>Image</td>
+<td>Exif.Image.InterColorProfile</td>
+<td>Undefined</td>
+<td>Contains an InterColor Consortium (ICC) format color space characterization/profile</td>
+</tr>
+<tr>
+<td>0x8822</td>
+<td>34850</td>
+<td>Image</td>
+<td>Exif.Image.ExposureProgram</td>
+<td>Short</td>
+<td>The class of the program used by the camera to set exposure when the picture is taken.</td>
+</tr>
+<tr>
+<td>0x8824</td>
+<td>34852</td>
+<td>Image</td>
+<td>Exif.Image.SpectralSensitivity</td>
+<td>Ascii</td>
+<td>Indicates the spectral sensitivity of each channel of the camera used.</td>
+</tr>
+<tr>
+<td>0x8825</td>
+<td>34853</td>
+<td>Image</td>
+<td>Exif.Image.GPSTag</td>
+<td>Long</td>
+<td>A pointer to the GPS Info IFD. The Interoperability structure of the GPS Info IFD, like that of Exif IFD, has no image data.</td>
+</tr>
+<tr>
+<td>0x8827</td>
+<td>34855</td>
+<td>Image</td>
+<td>Exif.Image.ISOSpeedRatings</td>
+<td>Short</td>
+<td>Indicates the ISO Speed and ISO Latitude of the camera or input device as specified in ISO 12232.</td>
+</tr>
+<tr>
+<td>0x8828</td>
+<td>34856</td>
+<td>Image</td>
+<td>Exif.Image.OECF</td>
+<td>Undefined</td>
+<td>Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524.</td>
+</tr>
+<tr>
+<td>0x8829</td>
+<td>34857</td>
+<td>Image</td>
+<td>Exif.Image.Interlace</td>
+<td>Short</td>
+<td>Indicates the field number of multifield images.</td>
+</tr>
+<tr>
+<td>0x882a</td>
+<td>34858</td>
+<td>Image</td>
+<td>Exif.Image.TimeZoneOffset</td>
+<td>SShort</td>
+<td>This optional tag encodes the time zone of the camera clock (relativeto Greenwich Mean Time) used to create the DataTimeOriginal tag-valuewhen the picture was taken. It may also contain the time zone offsetof the clock used to create the DateTime tag-value when the image wasmodified.</td>
+</tr>
+<tr>
+<td>0x882b</td>
+<td>34859</td>
+<td>Image</td>
+<td>Exif.Image.SelfTimerMode</td>
+<td>Short</td>
+<td>Number of seconds image capture was delayed from button press.</td>
+</tr>
+<tr>
+<td>0x9003</td>
+<td>36867</td>
+<td>Image</td>
+<td>Exif.Image.DateTimeOriginal</td>
+<td>Ascii</td>
+<td>The date and time when the original image data was generated.</td>
+</tr>
+<tr>
+<td>0x9102</td>
+<td>37122</td>
+<td>Image</td>
+<td>Exif.Image.CompressedBitsPerPixel</td>
+<td>Rational</td>
+<td>Specific to compressed data; states the compressed bits per pixel.</td>
+</tr>
+<tr>
+<td>0x9201</td>
+<td>37377</td>
+<td>Image</td>
+<td>Exif.Image.ShutterSpeedValue</td>
+<td>SRational</td>
+<td>Shutter speed.</td>
+</tr>
+<tr>
+<td>0x9202</td>
+<td>37378</td>
+<td>Image</td>
+<td>Exif.Image.ApertureValue</td>
+<td>Rational</td>
+<td>The lens aperture.</td>
+</tr>
+<tr>
+<td>0x9203</td>
+<td>37379</td>
+<td>Image</td>
+<td>Exif.Image.BrightnessValue</td>
+<td>SRational</td>
+<td>The value of brightness.</td>
+</tr>
+<tr>
+<td>0x9204</td>
+<td>37380</td>
+<td>Image</td>
+<td>Exif.Image.ExposureBiasValue</td>
+<td>SRational</td>
+<td>The exposure bias.</td>
+</tr>
+<tr>
+<td>0x9205</td>
+<td>37381</td>
+<td>Image</td>
+<td>Exif.Image.MaxApertureValue</td>
+<td>Rational</td>
+<td>The smallest F number of the lens.</td>
+</tr>
+<tr>
+<td>0x9206</td>
+<td>37382</td>
+<td>Image</td>
+<td>Exif.Image.SubjectDistance</td>
+<td>SRational</td>
+<td>The distance to the subject, given in meters.</td>
+</tr>
+<tr>
+<td>0x9207</td>
+<td>37383</td>
+<td>Image</td>
+<td>Exif.Image.MeteringMode</td>
+<td>Short</td>
+<td>The metering mode.</td>
+</tr>
+<tr>
+<td>0x9208</td>
+<td>37384</td>
+<td>Image</td>
+<td>Exif.Image.LightSource</td>
+<td>Short</td>
+<td>The kind of light source.</td>
+</tr>
+<tr>
+<td>0x9209</td>
+<td>37385</td>
+<td>Image</td>
+<td>Exif.Image.Flash</td>
+<td>Short</td>
+<td>Indicates the status of flash when the image was shot.</td>
+</tr>
+<tr>
+<td>0x920a</td>
+<td>37386</td>
+<td>Image</td>
+<td>Exif.Image.FocalLength</td>
+<td>Rational</td>
+<td>The actual focal length of the lens, in mm.</td>
+</tr>
+<tr>
+<td>0x920b</td>
+<td>37387</td>
+<td>Image</td>
+<td>Exif.Image.FlashEnergy</td>
+<td>Rational</td>
+<td>Amount of flash energy (BCPS).</td>
+</tr>
+<tr>
+<td>0x920c</td>
+<td>37388</td>
+<td>Image</td>
+<td>Exif.Image.SpatialFrequencyResponse</td>
+<td>Undefined</td>
+<td>SFR of the camera.</td>
+</tr>
+<tr>
+<td>0x920d</td>
+<td>37389</td>
+<td>Image</td>
+<td>Exif.Image.Noise</td>
+<td>Undefined</td>
+<td>Noise measurement values.</td>
+</tr>
+<tr>
+<td>0x920e</td>
+<td>37390</td>
+<td>Image</td>
+<td>Exif.Image.FocalPlaneXResolution</td>
+<td>Rational</td>
+<td>Number of pixels per FocalPlaneResolutionUnit (37392) in ImageWidth direction for main image.</td>
+</tr>
+<tr>
+<td>0x920f</td>
+<td>37391</td>
+<td>Image</td>
+<td>Exif.Image.FocalPlaneYResolution</td>
+<td>Rational</td>
+<td>Number of pixels per FocalPlaneResolutionUnit (37392) in ImageLength direction for main image.</td>
+</tr>
+<tr>
+<td>0x9210</td>
+<td>37392</td>
+<td>Image</td>
+<td>Exif.Image.FocalPlaneResolutionUnit</td>
+<td>Short</td>
+<td>Unit of measurement for FocalPlaneXResolution(37390) and FocalPlaneYResolution(37391).</td>
+</tr>
+<tr>
+<td>0x9211</td>
+<td>37393</td>
+<td>Image</td>
+<td>Exif.Image.ImageNumber</td>
+<td>Long</td>
+<td>Number assigned to an image, e.g., in a chained image burst.</td>
+</tr>
+<tr>
+<td>0x9212</td>
+<td>37394</td>
+<td>Image</td>
+<td>Exif.Image.SecurityClassification</td>
+<td>Ascii</td>
+<td>Security classification assigned to the image.</td>
+</tr>
+<tr>
+<td>0x9213</td>
+<td>37395</td>
+<td>Image</td>
+<td>Exif.Image.ImageHistory</td>
+<td>Ascii</td>
+<td>Record of what has been done to the image.</td>
+</tr>
+<tr>
+<td>0x9214</td>
+<td>37396</td>
+<td>Image</td>
+<td>Exif.Image.SubjectLocation</td>
+<td>Short</td>
+<td>Indicates the location and area of the main subject in the overall scene.</td>
+</tr>
+<tr>
+<td>0x9215</td>
+<td>37397</td>
+<td>Image</td>
+<td>Exif.Image.ExposureIndex</td>
+<td>Rational</td>
+<td>Encodes the camera exposure index setting when image was captured.</td>
+</tr>
+<tr>
+<td>0x9216</td>
+<td>37398</td>
+<td>Image</td>
+<td>Exif.Image.TIFFEPStandardID</td>
+<td>Byte</td>
+<td>Contains four ASCII characters representing the TIFF/EP standard version of a TIFF/EP file, eg '1', '0', '0', '0'</td>
+</tr>
+<tr>
+<td>0x9217</td>
+<td>37399</td>
+<td>Image</td>
+<td>Exif.Image.SensingMethod</td>
+<td>Short</td>
+<td>Type of image sensor.</td>
+</tr>
+<tr>
+<td>0x9c9b</td>
+<td>40091</td>
+<td>Image</td>
+<td>Exif.Image.XPTitle</td>
+<td>Byte</td>
+<td>Title tag used by Windows, encoded in UCS2</td>
+</tr>
+<tr>
+<td>0x9c9c</td>
+<td>40092</td>
+<td>Image</td>
+<td>Exif.Image.XPComment</td>
+<td>Byte</td>
+<td>Comment tag used by Windows, encoded in UCS2</td>
+</tr>
+<tr>
+<td>0x9c9d</td>
+<td>40093</td>
+<td>Image</td>
+<td>Exif.Image.XPAuthor</td>
+<td>Byte</td>
+<td>Author tag used by Windows, encoded in UCS2</td>
+</tr>
+<tr>
+<td>0x9c9e</td>
+<td>40094</td>
+<td>Image</td>
+<td>Exif.Image.XPKeywords</td>
+<td>Byte</td>
+<td>Keywords tag used by Windows, encoded in UCS2</td>
+</tr>
+<tr>
+<td>0x9c9f</td>
+<td>40095</td>
+<td>Image</td>
+<td>Exif.Image.XPSubject</td>
+<td>Byte</td>
+<td>Subject tag used by Windows, encoded in UCS2</td>
+</tr>
+<tr>
+<td>0xc4a5</td>
+<td>50341</td>
+<td>Image</td>
+<td>Exif.Image.PrintImageMatching</td>
+<td>Undefined</td>
+<td>Print Image Matching, description needed.</td>
+</tr>
+<tr>
+<td>0xc612</td>
+<td>50706</td>
+<td>Image</td>
+<td>Exif.Image.DNGVersion</td>
+<td>Byte</td>
+<td>This tag encodes the DNG four-tier version number. For files compliant with version 1.1.0.0 of the DNG specification, this tag should contain the bytes: 1, 1, 0, 0.</td>
+</tr>
+<tr>
+<td>0xc613</td>
+<td>50707</td>
+<td>Image</td>
+<td>Exif.Image.DNGBackwardVersion</td>
+<td>Byte</td>
+<td>This tag specifies the oldest version of the Digital Negative specification for which a file is compatible. Readers shouldnot attempt to read a file if this tag specifies a version number that is higher than the version number of the specification the reader was based on.  In addition to checking the version tags, readers should, for all tags, check the types, counts, and values, to verify it is able to correctly read the file.</td>
+</tr>
+<tr>
+<td>0xc614</td>
+<td>50708</td>
+<td>Image</td>
+<td>Exif.Image.UniqueCameraModel</td>
+<td>Ascii</td>
+<td>Defines a unique, non-localized name for the camera model that created the image in the raw file. This name should include the manufacturer's name to avoid conflicts, and should not be localized, even if the camera name itself is localized for different markets (see LocalizedCameraModel). This string may be used by reader software to index into per-model preferences and replacement profiles.</td>
+</tr>
+<tr>
+<td>0xc615</td>
+<td>50709</td>
+<td>Image</td>
+<td>Exif.Image.LocalizedCameraModel</td>
+<td>Byte</td>
+<td>Similar to the UniqueCameraModel field, except the name can be localized for different markets to match the localization of the camera name.</td>
+</tr>
+<tr>
+<td>0xc616</td>
+<td>50710</td>
+<td>Image</td>
+<td>Exif.Image.CFAPlaneColor</td>
+<td>Byte</td>
+<td>Provides a mapping between the values in the CFAPattern tag and the plane numbers in LinearRaw space. This is a required tag for non-RGB CFA images.</td>
+</tr>
+<tr>
+<td>0xc617</td>
+<td>50711</td>
+<td>Image</td>
+<td>Exif.Image.CFALayout</td>
+<td>Short</td>
+<td>Describes the spatial layout of the CFA.</td>
+</tr>
+<tr>
+<td>0xc618</td>
+<td>50712</td>
+<td>Image</td>
+<td>Exif.Image.LinearizationTable</td>
+<td>Short</td>
+<td>Describes a lookup table that maps stored values into linear values. This tag is typically used to increase compression ratios by storing the raw data in a non-linear, more visually uniform space with fewer total encoding levels. If SamplesPerPixel is not equal to one, this single table applies to all the samples for each pixel.</td>
+</tr>
+<tr>
+<td>0xc619</td>
+<td>50713</td>
+<td>Image</td>
+<td>Exif.Image.BlackLevelRepeatDim</td>
+<td>Short</td>
+<td>Specifies repeat pattern size for the BlackLevel tag.</td>
+</tr>
+<tr>
+<td>0xc61a</td>
+<td>50714</td>
+<td>Image</td>
+<td>Exif.Image.BlackLevel</td>
+<td>Rational</td>
+<td>Specifies the zero light (a.k.a. thermal black or black current) encoding level, as a repeating pattern. The origin of this pattern is the top-left corner of the ActiveArea rectangle. The values are stored in row-column-sample scan order.</td>
+</tr>
+<tr>
+<td>0xc61b</td>
+<td>50715</td>
+<td>Image</td>
+<td>Exif.Image.BlackLevelDeltaH</td>
+<td>SRational</td>
+<td>If the zero light encoding level is a function of the image column, BlackLevelDeltaH specifies the difference between the zero light encoding level for each column and the baseline zero light encoding level. If SamplesPerPixel is not equal to one, this single table applies to all the samples for each pixel.</td>
+</tr>
+<tr>
+<td>0xc61c</td>
+<td>50716</td>
+<td>Image</td>
+<td>Exif.Image.BlackLevelDeltaV</td>
+<td>SRational</td>
+<td>If the zero light encoding level is a function of the image row, this tag specifies the difference between the zero light encoding level for each row and the baseline zero light encoding level. If SamplesPerPixel is not equal to one, this single table applies to all the samples for each pixel.</td>
+</tr>
+<tr>
+<td>0xc61d</td>
+<td>50717</td>
+<td>Image</td>
+<td>Exif.Image.WhiteLevel</td>
+<td>Short</td>
+<td>This tag specifies the fully saturated encoding level for the raw sample values. Saturation is caused either by the sensor itself becoming highly non-linear in response, or by the camera's analog to digital converter clipping.</td>
+</tr>
+<tr>
+<td>0xc61e</td>
+<td>50718</td>
+<td>Image</td>
+<td>Exif.Image.DefaultScale</td>
+<td>Rational</td>
+<td>DefaultScale is required for cameras with non-square pixels. It specifies the default scale factors for each direction to convert the image to square pixels. Typically these factors are selected to approximately preserve total pixel count. For CFA images that use CFALayout equal to 2, 3, 4, or 5, such as the Fujifilm SuperCCD, these two values should usually differ by a factor of 2.0.</td>
+</tr>
+<tr>
+<td>0xc61f</td>
+<td>50719</td>
+<td>Image</td>
+<td>Exif.Image.DefaultCropOrigin</td>
+<td>Short</td>
+<td>Raw images often store extra pixels around the edges of the final image. These extra pixels help prevent interpolation artifacts near the edges of the final image. DefaultCropOrigin specifies the origin of the final image area, in raw image coordinates (i.e., before the DefaultScale has been applied), relative to the top-left corner of the ActiveArea rectangle.</td>
+</tr>
+<tr>
+<td>0xc620</td>
+<td>50720</td>
+<td>Image</td>
+<td>Exif.Image.DefaultCropSize</td>
+<td>Short</td>
+<td>Raw images often store extra pixels around the edges of the final image. These extra pixels help prevent interpolation artifacts near the edges of the final image. DefaultCropSize specifies the size of the final image area, in raw image coordinates (i.e., before the DefaultScale has been applied).</td>
+</tr>
+<tr>
+<td>0xc621</td>
+<td>50721</td>
+<td>Image</td>
+<td>Exif.Image.ColorMatrix1</td>
+<td>SRational</td>
+<td>ColorMatrix1 defines a transformation matrix that converts XYZ values to reference camera native color space values, under the first calibration illuminant. The matrix values are stored in row scan order. The ColorMatrix1 tag is required for all non-monochrome DNG files.</td>
+</tr>
+<tr>
+<td>0xc622</td>
+<td>50722</td>
+<td>Image</td>
+<td>Exif.Image.ColorMatrix2</td>
+<td>SRational</td>
+<td>ColorMatrix2 defines a transformation matrix that converts XYZ values to reference camera native color space values, under the second calibration illuminant. The matrix values are stored in row scan order.</td>
+</tr>
+<tr>
+<td>0xc623</td>
+<td>50723</td>
+<td>Image</td>
+<td>Exif.Image.CameraCalibration1</td>
+<td>SRational</td>
+<td>CameraCalibration1 defines a calibration matrix that transforms reference camera native space values to individual camera native space values under the first calibration illuminant. The matrix is stored in row scan order. This matrix is stored separately from the matrix specified by the ColorMatrix1 tag to allow raw converters to swap in replacement color matrices based on UniqueCameraModel tag, while still taking advantage of any per-individual camera calibration performed by the camera manufacturer.</td>
+</tr>
+<tr>
+<td>0xc624</td>
+<td>50724</td>
+<td>Image</td>
+<td>Exif.Image.CameraCalibration2</td>
+<td>SRational</td>
+<td>CameraCalibration2 defines a calibration matrix that transforms reference camera native space values to individual camera native space values under the second calibration illuminant. The matrix is stored in row scan order. This matrix is stored separately from the matrix specified by the ColorMatrix2 tag to allow raw converters to swap in replacement color matrices based on UniqueCameraModel tag, while still taking advantage of any per-individual camera calibration performed by the camera manufacturer.</td>
+</tr>
+<tr>
+<td>0xc625</td>
+<td>50725</td>
+<td>Image</td>
+<td>Exif.Image.ReductionMatrix1</td>
+<td>SRational</td>
+<td>ReductionMatrix1 defines a dimensionality reduction matrix for use as the first stage in converting color camera native space values to XYZ values, under the first calibration illuminant. This tag may only be used if ColorPlanes is greater than 3. The matrix is stored in row scan order.</td>
+</tr>
+<tr>
+<td>0xc626</td>
+<td>50726</td>
+<td>Image</td>
+<td>Exif.Image.ReductionMatrix2</td>
+<td>SRational</td>
+<td>ReductionMatrix2 defines a dimensionality reduction matrix for use as the first stage in converting color camera native space values to XYZ values, under the second calibration illuminant. This tag may only be used if ColorPlanes is greater than 3. The matrix is stored in row scan order.</td>
+</tr>
+<tr>
+<td>0xc627</td>
+<td>50727</td>
+<td>Image</td>
+<td>Exif.Image.AnalogBalance</td>
+<td>Rational</td>
+<td>Normally the stored raw values are not white balanced, since any digital white balancing will reduce the dynamic range of the final image if the user decides to later adjust the white balance; however, if camera hardware is capable of white balancing the color channels before the signal is digitized, it can improve the dynamic range of the final image. AnalogBalance defines the gain, either analog (recommended) or digital (not recommended) that has been applied the stored raw values.</td>
+</tr>
+<tr>
+<td>0xc628</td>
+<td>50728</td>
+<td>Image</td>
+<td>Exif.Image.AsShotNeutral</td>
+<td>Short</td>
+<td>Specifies the selected white balance at time of capture, encoded as the coordinates of a perfectly neutral color in linear reference space values. The inclusion of this tag precludes the inclusion of the AsShotWhiteXY tag.</td>
+</tr>
+<tr>
+<td>0xc629</td>
+<td>50729</td>
+<td>Image</td>
+<td>Exif.Image.AsShotWhiteXY</td>
+<td>Rational</td>
+<td>Specifies the selected white balance at time of capture, encoded as x-y chromaticity coordinates. The inclusion of this tag precludes the inclusion of the AsShotNeutral tag.</td>
+</tr>
+<tr>
+<td>0xc62a</td>
+<td>50730</td>
+<td>Image</td>
+<td>Exif.Image.BaselineExposure</td>
+<td>SRational</td>
+<td>Camera models vary in the trade-off they make between highlight headroom and shadow noise. Some leave a significant amount of highlight headroom during a normal exposure. This allows significant negative exposure compensation to be applied during raw conversion, but also means normal exposures will contain more shadow noise. Other models leave less headroom during normal exposures. This allows for less negative exposure compensation, but results in lower shadow noise for normal exposures. Because of these differences, a raw converter needs to vary the zero point of its exposure compensation control from model to model. BaselineExposure specifies by how much (in EV units) to move the zero point. Positive values result in brighter default results, while negative values result in darker default results.</td>
+</tr>
+<tr>
+<td>0xc62b</td>
+<td>50731</td>
+<td>Image</td>
+<td>Exif.Image.BaselineNoise</td>
+<td>Rational</td>
+<td>Specifies the relative noise level of the camera model at a baseline ISO value of 100, compared to a reference camera model. Since noise levels tend to vary approximately with the square root of the ISO value, a raw converter can use this value, combined with the current ISO, to estimate the relative noise level of the current image.</td>
+</tr>
+<tr>
+<td>0xc62c</td>
+<td>50732</td>
+<td>Image</td>
+<td>Exif.Image.BaselineSharpness</td>
+<td>Rational</td>
+<td>Specifies the relative amount of sharpening required for this camera model, compared to a reference camera model. Camera models vary in the strengths of their anti-aliasing filters. Cameras with weak or no filters require less sharpening than cameras with strong anti-aliasing filters.</td>
+</tr>
+<tr>
+<td>0xc62d</td>
+<td>50733</td>
+<td>Image</td>
+<td>Exif.Image.BayerGreenSplit</td>
+<td>Long</td>
+<td>Only applies to CFA images using a Bayer pattern filter array. This tag specifies, in arbitrary units, how closely the values of the green pixels in the blue/green rows track the values of the green pixels in the red/green rows. A value of zero means the two kinds of green pixels track closely, while a non-zero value means they sometimes diverge. The useful range for this tag is from 0 (no divergence) to about 5000 (quite large divergence).</td>
+</tr>
+<tr>
+<td>0xc62e</td>
+<td>50734</td>
+<td>Image</td>
+<td>Exif.Image.LinearResponseLimit</td>
+<td>Rational</td>
+<td>Some sensors have an unpredictable non-linearity in their response as they near the upper limit of their encoding range. This non-linearity results in color shifts in the highlight areas of the resulting image unless the raw converter compensates for this effect. LinearResponseLimit specifies the fraction of the encoding range above which the response may become significantly non-linear.</td>
+</tr>
+<tr>
+<td>0xc62f</td>
+<td>50735</td>
+<td>Image</td>
+<td>Exif.Image.CameraSerialNumber</td>
+<td>Ascii</td>
+<td>CameraSerialNumber contains the serial number of the camera or camera body that captured the image.</td>
+</tr>
+<tr>
+<td>0xc630</td>
+<td>50736</td>
+<td>Image</td>
+<td>Exif.Image.LensInfo</td>
+<td>Rational</td>
+<td>Contains information about the lens that captured the image. If the minimum f-stops are unknown, they should be encoded as 0/0.</td>
+</tr>
+<tr>
+<td>0xc631</td>
+<td>50737</td>
+<td>Image</td>
+<td>Exif.Image.ChromaBlurRadius</td>
+<td>Rational</td>
+<td>ChromaBlurRadius provides a hint to the DNG reader about how much chroma blur should be applied to the image. If this tag is omitted, the reader will use its default amount of chroma blurring. Normally this tag is only included for non-CFA images, since the amount of chroma blur required for mosaic images is highly dependent on the de-mosaic algorithm, in which case the DNG reader's default value is likely optimized for its particular de-mosaic algorithm.</td>
+</tr>
+<tr>
+<td>0xc632</td>
+<td>50738</td>
+<td>Image</td>
+<td>Exif.Image.AntiAliasStrength</td>
+<td>Rational</td>
+<td>Provides a hint to the DNG reader about how strong the camera's anti-alias filter is. A value of 0.0 means no anti-alias filter (i.e., the camera is prone to aliasing artifacts with some subjects), while a value of 1.0 means a strong anti-alias filter (i.e., the camera almost never has aliasing artifacts).</td>
+</tr>
+<tr>
+<td>0xc633</td>
+<td>50739</td>
+<td>Image</td>
+<td>Exif.Image.ShadowScale</td>
+<td>SRational</td>
+<td>This tag is used by Adobe Camera Raw to control the sensitivity of its 'Shadows' slider.</td>
+</tr>
+<tr>
+<td>0xc634</td>
+<td>50740</td>
+<td>Image</td>
+<td>Exif.Image.DNGPrivateData</td>
+<td>Byte</td>
+<td>Provides a way for camera manufacturers to store private data in the DNG file for use by their own raw converters, and to have that data preserved by programs that edit DNG files.</td>
+</tr>
+<tr>
+<td>0xc635</td>
+<td>50741</td>
+<td>Image</td>
+<td>Exif.Image.MakerNoteSafety</td>
+<td>Short</td>
+<td>MakerNoteSafety lets the DNG reader know whether the EXIF MakerNote tag is safe to preserve along with the rest of the EXIF data. File browsers and other image management software processing an image with a preserved MakerNote should be aware that any thumbnail image embedded in the MakerNote may be stale, and may not reflect the current state of the full size image.</td>
+</tr>
+<tr>
+<td>0xc65a</td>
+<td>50778</td>
+<td>Image</td>
+<td>Exif.Image.CalibrationIlluminant1</td>
+<td>Short</td>
+<td>The illuminant used for the first set of color calibration tags (ColorMatrix1, CameraCalibration1, ReductionMatrix1). The legal values for this tag are the same as the legal values for the LightSource EXIF tag.</td>
+</tr>
+<tr>
+<td>0xc65b</td>
+<td>50779</td>
+<td>Image</td>
+<td>Exif.Image.CalibrationIlluminant2</td>
+<td>Short</td>
+<td>The illuminant used for an optional second set of color calibration tags (ColorMatrix2, CameraCalibration2, ReductionMatrix2). The legal values for this tag are the same as the legal values for the CalibrationIlluminant1 tag; however, if both are included, neither is allowed to have a value of 0 (unknown).</td>
+</tr>
+<tr>
+<td>0xc65c</td>
+<td>50780</td>
+<td>Image</td>
+<td>Exif.Image.BestQualityScale</td>
+<td>Rational</td>
+<td>For some cameras, the best possible image quality is not achieved by preserving the total pixel count during conversion. For example, Fujifilm SuperCCD images have maximum detail when their total pixel count is doubled. This tag specifies the amount by which the values of the DefaultScale tag need to be multiplied to achieve the best quality image size.</td>
+</tr>
+<tr>
+<td>0xc65d</td>
+<td>50781</td>
+<td>Image</td>
+<td>Exif.Image.RawDataUniqueID</td>
+<td>Byte</td>
+<td>This tag contains a 16-byte unique identifier for the raw image data in the DNG file. DNG readers can use this tag to recognize a particular raw image, even if the file's name or the metadata contained in the file has been changed. If a DNG writer creates such an identifier, it should do so using an algorithm that will ensure that it is very unlikely two different images will end up having the same identifier.</td>
+</tr>
+<tr>
+<td>0xc68b</td>
+<td>50827</td>
+<td>Image</td>
+<td>Exif.Image.OriginalRawFileName</td>
+<td>Byte</td>
+<td>If the DNG file was converted from a non-DNG raw file, then this tag contains the file name of that original raw file.</td>
+</tr>
+<tr>
+<td>0xc68c</td>
+<td>50828</td>
+<td>Image</td>
+<td>Exif.Image.OriginalRawFileData</td>
+<td>Undefined</td>
+<td>If the DNG file was converted from a non-DNG raw file, then this tag contains the compressed contents of that original raw file. The contents of this tag always use the big-endian byte order. The tag contains a sequence of data blocks. Future versions of the DNG specification may define additional data blocks, so DNG readers should ignore extra bytes when parsing this tag. DNG readers should also detect the case where data blocks are missing from the end of the sequence, and should assume a default value for all the missing blocks. There are no padding or alignment bytes between data blocks.</td>
+</tr>
+<tr>
+<td>0xc68d</td>
+<td>50829</td>
+<td>Image</td>
+<td>Exif.Image.ActiveArea</td>
+<td>Short</td>
+<td>This rectangle defines the active (non-masked) pixels of the sensor. The order of the rectangle coordinates is: top, left, bottom, right.</td>
+</tr>
+<tr>
+<td>0xc68e</td>
+<td>50830</td>
+<td>Image</td>
+<td>Exif.Image.MaskedAreas</td>
+<td>Short</td>
+<td>This tag contains a list of non-overlapping rectangle coordinates of fully masked pixels, which can be optionally used by DNG readers to measure the black encoding level. The order of each rectangle's coordinates is: top, left, bottom, right. If the raw image data has already had its black encoding level subtracted, then this tag should not be used, since the masked pixels are no longer useful.</td>
+</tr>
+<tr>
+<td>0xc68f</td>
+<td>50831</td>
+<td>Image</td>
+<td>Exif.Image.AsShotICCProfile</td>
+<td>Undefined</td>
+<td>This tag contains an ICC profile that, in conjunction with the AsShotPreProfileMatrix tag, provides the camera manufacturer with a way to specify a default color rendering from camera color space coordinates (linear reference values) into the ICC profile connection space. The ICC profile connection space is an output referred colorimetric space, whereas the other color calibration tags in DNG specify a conversion into a scene referred colorimetric space. This means that the rendering in this profile should include any desired tone and gamut mapping needed to convert between scene referred values and output referred values.</td>
+</tr>
+<tr>
+<td>0xc690</td>
+<td>50832</td>
+<td>Image</td>
+<td>Exif.Image.AsShotPreProfileMatrix</td>
+<td>SRational</td>
+<td>This tag is used in conjunction with the AsShotICCProfile tag. It specifies a matrix that should be applied to the camera color space coordinates before processing the values through the ICC profile specified in the AsShotICCProfile tag. The matrix is stored in the row scan order. If ColorPlanes is greater than three, then this matrix can (but is not required to) reduce the dimensionality of the color data down to three components, in which case the AsShotICCProfile should have three rather than ColorPlanes input components.</td>
+</tr>
+<tr>
+<td>0xc691</td>
+<td>50833</td>
+<td>Image</td>
+<td>Exif.Image.CurrentICCProfile</td>
+<td>Undefined</td>
+<td>This tag is used in conjunction with the CurrentPreProfileMatrix tag. The CurrentICCProfile and CurrentPreProfileMatrix tags have the same purpose and usage as the AsShotICCProfile and AsShotPreProfileMatrix tag pair, except they are for use by raw file editors rather than camera manufacturers.</td>
+</tr>
+<tr>
+<td>0xc692</td>
+<td>50834</td>
+<td>Image</td>
+<td>Exif.Image.CurrentPreProfileMatrix</td>
+<td>SRational</td>
+<td>This tag is used in conjunction with the CurrentICCProfile tag. The CurrentICCProfile and CurrentPreProfileMatrix tags have the same purpose and usage as the AsShotICCProfile and AsShotPreProfileMatrix tag pair, except they are for use by raw file editors rather than camera manufacturers.</td>
+</tr>
+<tr>
+<td>0xc6bf</td>
+<td>50879</td>
+<td>Image</td>
+<td>Exif.Image.ColorimetricReference</td>
+<td>Short</td>
+<td>The DNG color model documents a transform between camera colors and CIE XYZ values. This tag describes the colorimetric reference for the CIE XYZ values. 0 = The XYZ values are scene-referred. 1 = The XYZ values are output-referred, using the ICC profile perceptual dynamic range. This tag allows output-referred data to be stored in DNG files and still processed correctly by DNG readers.</td>
+</tr>
+<tr>
+<td>0xc6f3</td>
+<td>50931</td>
+<td>Image</td>
+<td>Exif.Image.CameraCalibrationSignature</td>
+<td>Byte</td>
+<td>A UTF-8 encoded string associated with the CameraCalibration1 and CameraCalibration2 tags. The CameraCalibration1 and CameraCalibration2 tags should only be used in the DNG color transform if the string stored in the CameraCalibrationSignature tag exactly matches the string stored in the ProfileCalibrationSignature tag for the selected camera profile.</td>
+</tr>
+<tr>
+<td>0xc6f4</td>
+<td>50932</td>
+<td>Image</td>
+<td>Exif.Image.ProfileCalibrationSignature</td>
+<td>Byte</td>
+<td>A UTF-8 encoded string associated with the camera profile tags. The CameraCalibration1 and CameraCalibration2 tags should only be used in the DNG color transfer if the string stored in the CameraCalibrationSignature tag exactly matches the string stored in the ProfileCalibrationSignature tag for the selected camera profile.</td>
+</tr>
+<tr>
+<td>0xc6f6</td>
+<td>50934</td>
+<td>Image</td>
+<td>Exif.Image.AsShotProfileName</td>
+<td>Byte</td>
+<td>A UTF-8 encoded string containing the name of the "as shot" camera profile, if any.</td>
+</tr>
+<tr>
+<td>0xc6f7</td>
+<td>50935</td>
+<td>Image</td>
+<td>Exif.Image.NoiseReductionApplied</td>
+<td>Rational</td>
+<td>This tag indicates how much noise reduction has been applied to the raw data on a scale of 0.0 to 1.0. A 0.0 value indicates that no noise reduction has been applied. A 1.0 value indicates that the "ideal" amount of noise reduction has been applied, i.e. that the DNG reader should not apply additional noise reduction by default. A value of 0/0 indicates that this parameter is unknown.</td>
+</tr>
+<tr>
+<td>0xc6f8</td>
+<td>50936</td>
+<td>Image</td>
+<td>Exif.Image.ProfileName</td>
+<td>Byte</td>
+<td>A UTF-8 encoded string containing the name of the camera profile. This tag is optional if there is only a single camera profile stored in the file but is required for all camera profiles if there is more than one camera profile stored in the file.</td>
+</tr>
+<tr>
+<td>0xc6f9</td>
+<td>50937</td>
+<td>Image</td>
+<td>Exif.Image.ProfileHueSatMapDims</td>
+<td>Long</td>
+<td>This tag specifies the number of input samples in each dimension of the hue/saturation/value mapping tables. The data for these tables are stored in ProfileHueSatMapData1 and ProfileHueSatMapData2 tags. The most common case has ValueDivisions equal to 1, so only hue and saturation are used as inputs to the mapping table.</td>
+</tr>
+<tr>
+<td>0xc6fa</td>
+<td>50938</td>
+<td>Image</td>
+<td>Exif.Image.ProfileHueSatMapData1</td>
+<td>Float</td>
+<td>This tag contains the data for the first hue/saturation/value mapping table. Each entry of the table contains three 32-bit IEEE floating-point values. The first entry is hue shift in degrees; the second entry is saturation scale factor; and the third entry is a value scale factor. The table entries are stored in the tag in nested loop order, with the value divisions in the outer loop, the hue divisions in the middle loop, and the saturation divisions in the inner loop. All zero input saturation entries are required to have a value scale factor of 1.0.</td>
+</tr>
+<tr>
+<td>0xc6fb</td>
+<td>50939</td>
+<td>Image</td>
+<td>Exif.Image.ProfileHueSatMapData2</td>
+<td>Float</td>
+<td>This tag contains the data for the second hue/saturation/value mapping table. Each entry of the table contains three 32-bit IEEE floating-point values. The first entry is hue shift in degrees; the second entry is a saturation scale factor; and the third entry is a value scale factor. The table entries are stored in the tag in nested loop order, with the value divisions in the outer loop, the hue divisions in the middle loop, and the saturation divisions in the inner loop. All zero input saturation entries are required to have a value scale factor of 1.0.</td>
+</tr>
+<tr>
+<td>0xc6fc</td>
+<td>50940</td>
+<td>Image</td>
+<td>Exif.Image.ProfileToneCurve</td>
+<td>Float</td>
+<td>This tag contains a default tone curve that can be applied while processing the image as a starting point for user adjustments. The curve is specified as a list of 32-bit IEEE floating-point value pairs in linear gamma. Each sample has an input value in the range of 0.0 to 1.0, and an output value in the range of 0.0 to 1.0. The first sample is required to be (0.0, 0.0), and the last sample is required to be (1.0, 1.0). Interpolated the curve using a cubic spline.</td>
+</tr>
+<tr>
+<td>0xc6fd</td>
+<td>50941</td>
+<td>Image</td>
+<td>Exif.Image.ProfileEmbedPolicy</td>
+<td>Long</td>
+<td>This tag contains information about the usage rules for the associated camera profile.</td>
+</tr>
+<tr>
+<td>0xc6fe</td>
+<td>50942</td>
+<td>Image</td>
+<td>Exif.Image.ProfileCopyright</td>
+<td>Byte</td>
+<td>A UTF-8 encoded string containing the copyright information for the camera profile. This string always should be preserved along with the other camera profile tags.</td>
+</tr>
+<tr>
+<td>0xc714</td>
+<td>50964</td>
+<td>Image</td>
+<td>Exif.Image.ForwardMatrix1</td>
+<td>SRational</td>
+<td>This tag defines a matrix that maps white balanced camera colors to XYZ D50 colors.</td>
+</tr>
+<tr>
+<td>0xc715</td>
+<td>50965</td>
+<td>Image</td>
+<td>Exif.Image.ForwardMatrix2</td>
+<td>SRational</td>
+<td>This tag defines a matrix that maps white balanced camera colors to XYZ D50 colors.</td>
+</tr>
+<tr>
+<td>0xc716</td>
+<td>50966</td>
+<td>Image</td>
+<td>Exif.Image.PreviewApplicationName</td>
+<td>Byte</td>
+<td>A UTF-8 encoded string containing the name of the application that created the preview stored in the IFD.</td>
+</tr>
+<tr>
+<td>0xc717</td>
+<td>50967</td>
+<td>Image</td>
+<td>Exif.Image.PreviewApplicationVersion</td>
+<td>Byte</td>
+<td>A UTF-8 encoded string containing the version number of the application that created the preview stored in the IFD.</td>
+</tr>
+<tr>
+<td>0xc718</td>
+<td>50968</td>
+<td>Image</td>
+<td>Exif.Image.PreviewSettingsName</td>
+<td>Byte</td>
+<td>A UTF-8 encoded string containing the name of the conversion settings (for example, snapshot name) used for the preview stored in the IFD.</td>
+</tr>
+<tr>
+<td>0xc719</td>
+<td>50969</td>
+<td>Image</td>
+<td>Exif.Image.PreviewSettingsDigest</td>
+<td>Byte</td>
+<td>A unique ID of the conversion settings (for example, MD5 digest) used to render the preview stored in the IFD.</td>
+</tr>
+<tr>
+<td>0xc71a</td>
+<td>50970</td>
+<td>Image</td>
+<td>Exif.Image.PreviewColorSpace</td>
+<td>Long</td>
+<td>This tag specifies the color space in which the rendered preview in this IFD is stored. The default value for this tag is sRGB for color previews and Gray Gamma 2.2 for monochrome previews.</td>
+</tr>
+<tr>
+<td>0xc71b</td>
+<td>50971</td>
+<td>Image</td>
+<td>Exif.Image.PreviewDateTime</td>
+<td>Ascii</td>
+<td>This tag is an ASCII string containing the name of the date/time at which the preview stored in the IFD was rendered. The date/time is encoded using ISO 8601 format.</td>
+</tr>
+<tr>
+<td>0xc71c</td>
+<td>50972</td>
+<td>Image</td>
+<td>Exif.Image.RawImageDigest</td>
+<td>Undefined</td>
+<td>This tag is an MD5 digest of the raw image data. All pixels in the image are processed in row-scan order. Each pixel is zero padded to 16 or 32 bits deep (16-bit for data less than or equal to 16 bits deep, 32-bit otherwise). The data for each pixel is processed in little-endian byte order.</td>
+</tr>
+<tr>
+<td>0xc71d</td>
+<td>50973</td>
+<td>Image</td>
+<td>Exif.Image.OriginalRawFileDigest</td>
+<td>Undefined</td>
+<td>This tag is an MD5 digest of the data stored in the OriginalRawFileData tag.</td>
+</tr>
+<tr>
+<td>0xc71e</td>
+<td>50974</td>
+<td>Image</td>
+<td>Exif.Image.SubTileBlockSize</td>
+<td>Long</td>
+<td>Normally, the pixels within a tile are stored in simple row-scan order. This tag specifies that the pixels within a tile should be grouped first into rectangular blocks of the specified size. These blocks are stored in row-scan order. Within each block, the pixels are stored in row-scan order. The use of a non-default value for this tag requires setting the DNGBackwardVersion tag to at least 1.2.0.0.</td>
+</tr>
+<tr>
+<td>0xc71f</td>
+<td>50975</td>
+<td>Image</td>
+<td>Exif.Image.RowInterleaveFactor</td>
+<td>Long</td>
+<td>This tag specifies that rows of the image are stored in interleaved order. The value of the tag specifies the number of interleaved fields. The use of a non-default value for this tag requires setting the DNGBackwardVersion tag to at least 1.2.0.0.</td>
+</tr>
+<tr>
+<td>0xc725</td>
+<td>50981</td>
+<td>Image</td>
+<td>Exif.Image.ProfileLookTableDims</td>
+<td>Long</td>
+<td>This tag specifies the number of input samples in each dimension of a default "look" table. The data for this table is stored in the ProfileLookTableData tag.</td>
+</tr>
+<tr>
+<td>0xc726</td>
+<td>50982</td>
+<td>Image</td>
+<td>Exif.Image.ProfileLookTableData</td>
+<td>Float</td>
+<td>This tag contains a default "look" table that can be applied while processing the image as a starting point for user adjustment. This table uses the same format as the tables stored in the ProfileHueSatMapData1 and ProfileHueSatMapData2 tags, and is applied in the same color space. However, it should be applied later in the processing pipe, after any exposure compensation and/or fill light stages, but before any tone curve stage. Each entry of the table contains three 32-bit IEEE floating-point values. The first entry is hue shift in degrees, the second entry is a saturation scale factor, and the third entry is a value scale factor. The table entries are stored in the tag in nested loop order, with the value divisions in the outer loop, the hue divisions in the middle loop, and the saturation divisions in the inner loop. All zero input saturation entries are required to have a value scale factor of 1.0.</td>
+</tr>
+<tr>
+<td>0xc740</td>
+<td>51008</td>
+<td>Image</td>
+<td>Exif.Image.OpcodeList1</td>
+<td>Undefined</td>
+<td>Specifies the list of opcodes that should be applied to the raw image, as read directly from the file.</td>
+</tr>
+<tr>
+<td>0xc741</td>
+<td>51009</td>
+<td>Image</td>
+<td>Exif.Image.OpcodeList2</td>
+<td>Undefined</td>
+<td>Specifies the list of opcodes that should be applied to the raw image, just after it has been mapped to linear reference values.</td>
+</tr>
+<tr>
+<td>0xc74e</td>
+<td>51022</td>
+<td>Image</td>
+<td>Exif.Image.OpcodeList3</td>
+<td>Undefined</td>
+<td>Specifies the list of opcodes that should be applied to the raw image, just after it has been demosaiced.</td>
+</tr>
+<tr>
+<td>0xc761</td>
+<td>51041</td>
+<td>Image</td>
+<td>Exif.Image.NoiseProfile</td>
+<td>Double</td>
+<td>NoiseProfile describes the amount of noise in a raw image. Specifically, this tag models the amount of signal-dependent photon (shot) noise and signal-independent sensor readout noise, two common sources of noise in raw images. The model assumes that the noise is white and spatially independent, ignoring fixed pattern effects and other sources of noise (e.g., pixel response non-uniformity, spatially-dependent thermal effects, etc.).</td>
+</tr>
+<tr>
+<td>0x829a</td>
+<td>33434</td>
+<td>Photo</td>
+<td>Exif.Photo.ExposureTime</td>
+<td>Rational</td>
+<td>Exposure time, given in seconds (sec).</td>
+</tr>
+<tr>
+<td>0x829d</td>
+<td>33437</td>
+<td>Photo</td>
+<td>Exif.Photo.FNumber</td>
+<td>Rational</td>
+<td>The F number.</td>
+</tr>
+<tr>
+<td>0x8822</td>
+<td>34850</td>
+<td>Photo</td>
+<td>Exif.Photo.ExposureProgram</td>
+<td>Short</td>
+<td>The class of the program used by the camera to set exposure when the picture is taken.</td>
+</tr>
+<tr>
+<td>0x8824</td>
+<td>34852</td>
+<td>Photo</td>
+<td>Exif.Photo.SpectralSensitivity</td>
+<td>Ascii</td>
+<td>Indicates the spectral sensitivity of each channel of the camera used. The tag value is an ASCII string compatible with the standard developed by the ASTM Technical Committee.</td>
+</tr>
+<tr>
+<td>0x8827</td>
+<td>34855</td>
+<td>Photo</td>
+<td>Exif.Photo.ISOSpeedRatings</td>
+<td>Short</td>
+<td>Indicates the ISO Speed and ISO Latitude of the camera or input device as specified in ISO 12232.</td>
+</tr>
+<tr>
+<td>0x8828</td>
+<td>34856</td>
+<td>Photo</td>
+<td>Exif.Photo.OECF</td>
+<td>Undefined</td>
+<td>Indicates the Opto-Electoric Conversion Function (OECF) specified in ISO 14524. &lt;OECF&gt; is the relationship between the camera optical input and the image values.</td>
+</tr>
+<tr>
+<td>0x8830</td>
+<td>34864</td>
+<td>Photo</td>
+<td>Exif.Photo.SensitivityType</td>
+<td>Short</td>
+<td>The SensitivityType tag indicates which one of the parameters of ISO12232 is the PhotographicSensitivity tag. Although it is an optional tag, it should be recorded when a PhotographicSensitivity tag is recorded. Value = 4, 5, 6, or 7 may be used in case that the values of plural parameters are the same.</td>
+</tr>
+<tr>
+<td>0x8831</td>
+<td>34865</td>
+<td>Photo</td>
+<td>Exif.Photo.StandardOutputSensitivity</td>
+<td>Long</td>
+<td>This tag indicates the standard output sensitivity value of a camera or input device defined in ISO 12232. When recording this tag, the PhotographicSensitivity and SensitivityType tags shall also be recorded.</td>
+</tr>
+<tr>
+<td>0x8832</td>
+<td>34866</td>
+<td>Photo</td>
+<td>Exif.Photo.RecommendedExposureIndex</td>
+<td>Long</td>
+<td>This tag indicates the recommended exposure index value of a camera or input device defined in ISO 12232. When recording this tag, the PhotographicSensitivity and SensitivityType tags shall also be recorded.</td>
+</tr>
+<tr>
+<td>0x8833</td>
+<td>34867</td>
+<td>Photo</td>
+<td>Exif.Photo.ISOSpeed</td>
+<td>Long</td>
+<td>This tag indicates the ISO speed value of a camera or input device that is defined in ISO 12232. When recording this tag, the PhotographicSensitivity and SensitivityType tags shall also be recorded.</td>
+</tr>
+<tr>
+<td>0x8834</td>
+<td>34868</td>
+<td>Photo</td>
+<td>Exif.Photo.ISOSpeedLatitudeyyy</td>
+<td>Long</td>
+<td>This tag indicates the ISO speed latitude yyy value of a camera or input device that is defined in ISO 12232. However, this tag shall not be recorded without ISOSpeed and ISOSpeedLatitudezzz.</td>
+</tr>
+<tr>
+<td>0x8835</td>
+<td>34869</td>
+<td>Photo</td>
+<td>Exif.Photo.ISOSpeedLatitudezzz</td>
+<td>Long</td>
+<td>This tag indicates the ISO speed latitude zzz value of a camera or input device that is defined in ISO 12232. However, this tag shall not be recorded without ISOSpeed and ISOSpeedLatitudeyyy.</td>
+</tr>
+<tr>
+<td>0x9000</td>
+<td>36864</td>
+<td>Photo</td>
+<td>Exif.Photo.ExifVersion</td>
+<td>Undefined</td>
+<td>The version of this standard supported. Nonexistence of this field is taken to mean nonconformance to the standard.</td>
+</tr>
+<tr>
+<td>0x9003</td>
+<td>36867</td>
+<td>Photo</td>
+<td>Exif.Photo.DateTimeOriginal</td>
+<td>Ascii</td>
+<td>The date and time when the original image data was generated. For a digital still camera the date and time the picture was taken are recorded.</td>
+</tr>
+<tr>
+<td>0x9004</td>
+<td>36868</td>
+<td>Photo</td>
+<td>Exif.Photo.DateTimeDigitized</td>
+<td>Ascii</td>
+<td>The date and time when the image was stored as digital data.</td>
+</tr>
+<tr>
+<td>0x9101</td>
+<td>37121</td>
+<td>Photo</td>
+<td>Exif.Photo.ComponentsConfiguration</td>
+<td>Undefined</td>
+<td>Information specific to compressed data. The channels of each component are arranged in order from the 1st component to the 4th. For uncompressed data the data arrangement is given in the &lt;PhotometricInterpretation&gt; tag. However, since &lt;PhotometricInterpretation&gt; can only express the order of Y, Cb and Cr, this tag is provided for cases when compressed data uses components other than Y, Cb, and Cr and to enable support of other sequences.</td>
+</tr>
+<tr>
+<td>0x9102</td>
+<td>37122</td>
+<td>Photo</td>
+<td>Exif.Photo.CompressedBitsPerPixel</td>
+<td>Rational</td>
+<td>Information specific to compressed data. The compression mode used for a compressed image is indicated in unit bits per pixel.</td>
+</tr>
+<tr>
+<td>0x9201</td>
+<td>37377</td>
+<td>Photo</td>
+<td>Exif.Photo.ShutterSpeedValue</td>
+<td>SRational</td>
+<td>Shutter speed. The unit is the APEX (Additive System of Photographic Exposure) setting.</td>
+</tr>
+<tr>
+<td>0x9202</td>
+<td>37378</td>
+<td>Photo</td>
+<td>Exif.Photo.ApertureValue</td>
+<td>Rational</td>
+<td>The lens aperture. The unit is the APEX value.</td>
+</tr>
+<tr>
+<td>0x9203</td>
+<td>37379</td>
+<td>Photo</td>
+<td>Exif.Photo.BrightnessValue</td>
+<td>SRational</td>
+<td>The value of brightness. The unit is the APEX value. Ordinarily it is given in the range of -99.99 to 99.99.</td>
+</tr>
+<tr>
+<td>0x9204</td>
+<td>37380</td>
+<td>Photo</td>
+<td>Exif.Photo.ExposureBiasValue</td>
+<td>SRational</td>
+<td>The exposure bias. The units is the APEX value. Ordinarily it is given in the range of -99.99 to 99.99.</td>
+</tr>
+<tr>
+<td>0x9205</td>
+<td>37381</td>
+<td>Photo</td>
+<td>Exif.Photo.MaxApertureValue</td>
+<td>Rational</td>
+<td>The smallest F number of the lens. The unit is the APEX value. Ordinarily it is given in the range of 00.00 to 99.99, but it is not limited to this range.</td>
+</tr>
+<tr>
+<td>0x9206</td>
+<td>37382</td>
+<td>Photo</td>
+<td>Exif.Photo.SubjectDistance</td>
+<td>Rational</td>
+<td>The distance to the subject, given in meters.</td>
+</tr>
+<tr>
+<td>0x9207</td>
+<td>37383</td>
+<td>Photo</td>
+<td>Exif.Photo.MeteringMode</td>
+<td>Short</td>
+<td>The metering mode.</td>
+</tr>
+<tr>
+<td>0x9208</td>
+<td>37384</td>
+<td>Photo</td>
+<td>Exif.Photo.LightSource</td>
+<td>Short</td>
+<td>The kind of light source.</td>
+</tr>
+<tr>
+<td>0x9209</td>
+<td>37385</td>
+<td>Photo</td>
+<td>Exif.Photo.Flash</td>
+<td>Short</td>
+<td>This tag is recorded when an image is taken using a strobe light (flash).</td>
+</tr>
+<tr>
+<td>0x920a</td>
+<td>37386</td>
+<td>Photo</td>
+<td>Exif.Photo.FocalLength</td>
+<td>Rational</td>
+<td>The actual focal length of the lens, in mm. Conversion is not made to the focal length of a 35 mm film camera.</td>
+</tr>
+<tr>
+<td>0x9214</td>
+<td>37396</td>
+<td>Photo</td>
+<td>Exif.Photo.SubjectArea</td>
+<td>Short</td>
+<td>This tag indicates the location and area of the main subject in the overall scene.</td>
+</tr>
+<tr>
+<td>0x927c</td>
+<td>37500</td>
+<td>Photo</td>
+<td>Exif.Photo.MakerNote</td>
+<td>Undefined</td>
+<td>A tag for manufacturers of Exif writers to record any desired information. The contents are up to the manufacturer.</td>
+</tr>
+<tr>
+<td>0x9286</td>
+<td>37510</td>
+<td>Photo</td>
+<td>Exif.Photo.UserComment</td>
+<td>Comment</td>
+<td>A tag for Exif users to write keywords or comments on the image besides those in &lt;ImageDescription&gt;, and without the character code limitations of the &lt;ImageDescription&gt; tag.</td>
+</tr>
+<tr>
+<td>0x9290</td>
+<td>37520</td>
+<td>Photo</td>
+<td>Exif.Photo.SubSecTime</td>
+<td>Ascii</td>
+<td>A tag used to record fractions of seconds for the &lt;DateTime&gt; tag.</td>
+</tr>
+<tr>
+<td>0x9291</td>
+<td>37521</td>
+<td>Photo</td>
+<td>Exif.Photo.SubSecTimeOriginal</td>
+<td>Ascii</td>
+<td>A tag used to record fractions of seconds for the &lt;DateTimeOriginal&gt; tag.</td>
+</tr>
+<tr>
+<td>0x9292</td>
+<td>37522</td>
+<td>Photo</td>
+<td>Exif.Photo.SubSecTimeDigitized</td>
+<td>Ascii</td>
+<td>A tag used to record fractions of seconds for the &lt;DateTimeDigitized&gt; tag.</td>
+</tr>
+<tr>
+<td>0xa000</td>
+<td>40960</td>
+<td>Photo</td>
+<td>Exif.Photo.FlashpixVersion</td>
+<td>Undefined</td>
+<td>The FlashPix format version supported by a FPXR file.</td>
+</tr>
+<tr>
+<td>0xa001</td>
+<td>40961</td>
+<td>Photo</td>
+<td>Exif.Photo.ColorSpace</td>
+<td>Short</td>
+<td>The color space information tag is always recorded as the color space specifier. Normally sRGB is used to define the color space based on the PC monitor conditions and environment. If a color space other than sRGB is used, Uncalibrated is set. Image data recorded as Uncalibrated can be treated as sRGB when it is converted to FlashPix.</td>
+</tr>
+<tr>
+<td>0xa002</td>
+<td>40962</td>
+<td>Photo</td>
+<td>Exif.Photo.PixelXDimension</td>
+<td>Long</td>
+<td>Information specific to compressed data. When a compressed file is recorded, the valid width of the meaningful image must be recorded in this tag, whether or not there is padding data or a restart marker. This tag should not exist in an uncompressed file.</td>
+</tr>
+<tr>
+<td>0xa003</td>
+<td>40963</td>
+<td>Photo</td>
+<td>Exif.Photo.PixelYDimension</td>
+<td>Long</td>
+<td>Information specific to compressed data. When a compressed file is recorded, the valid height of the meaningful image must be recorded in this tag, whether or not there is padding data or a restart marker. This tag should not exist in an uncompressed file. Since data padding is unnecessary in the vertical direction, the number of lines recorded in this valid image height tag will in fact be the same as that recorded in the SOF.</td>
+</tr>
+<tr>
+<td>0xa004</td>
+<td>40964</td>
+<td>Photo</td>
+<td>Exif.Photo.RelatedSoundFile</td>
+<td>Ascii</td>
+<td>This tag is used to record the name of an audio file related to the image data. The only relational information recorded here is the Exif audio file name and extension (an ASCII string consisting of 8 characters + '.' + 3 characters). The path is not recorded.</td>
+</tr>
+<tr>
+<td>0xa005</td>
+<td>40965</td>
+<td>Photo</td>
+<td>Exif.Photo.InteroperabilityTag</td>
+<td>Long</td>
+<td>Interoperability IFD is composed of tags which stores the information to ensure the Interoperability and pointed by the following tag located in Exif IFD. The Interoperability structure of Interoperability IFD is the same as TIFF defined IFD structure but does not contain the image data characteristically compared with normal TIFF IFD.</td>
+</tr>
+<tr>
+<td>0xa20b</td>
+<td>41483</td>
+<td>Photo</td>
+<td>Exif.Photo.FlashEnergy</td>
+<td>Rational</td>
+<td>Indicates the strobe energy at the time the image is captured, as measured in Beam Candle Power Seconds (BCPS).</td>
+</tr>
+<tr>
+<td>0xa20c</td>
+<td>41484</td>
+<td>Photo</td>
+<td>Exif.Photo.SpatialFrequencyResponse</td>
+<td>Undefined</td>
+<td>This tag records the camera or input device spatial frequency table and SFR values in the direction of image width, image height, and diagonal direction, as specified in ISO 12233.</td>
+</tr>
+<tr>
+<td>0xa20e</td>
+<td>41486</td>
+<td>Photo</td>
+<td>Exif.Photo.FocalPlaneXResolution</td>
+<td>Rational</td>
+<td>Indicates the number of pixels in the image width (X) direction per &lt;FocalPlaneResolutionUnit&gt; on the camera focal plane.</td>
+</tr>
+<tr>
+<td>0xa20f</td>
+<td>41487</td>
+<td>Photo</td>
+<td>Exif.Photo.FocalPlaneYResolution</td>
+<td>Rational</td>
+<td>Indicates the number of pixels in the image height (V) direction per &lt;FocalPlaneResolutionUnit&gt; on the camera focal plane.</td>
+</tr>
+<tr>
+<td>0xa210</td>
+<td>41488</td>
+<td>Photo</td>
+<td>Exif.Photo.FocalPlaneResolutionUnit</td>
+<td>Short</td>
+<td>Indicates the unit for measuring &lt;FocalPlaneXResolution&gt; and &lt;FocalPlaneYResolution&gt;. This value is the same as the &lt;ResolutionUnit&gt;.</td>
+</tr>
+<tr>
+<td>0xa214</td>
+<td>41492</td>
+<td>Photo</td>
+<td>Exif.Photo.SubjectLocation</td>
+<td>Short</td>
+<td>Indicates the location of the main subject in the scene. The value of this tag represents the pixel at the center of the main subject relative to the left edge, prior to rotation processing as per the &lt;Rotation&gt; tag. The first value indicates the X column number and second indicates the Y row number.</td>
+</tr>
+<tr>
+<td>0xa215</td>
+<td>41493</td>
+<td>Photo</td>
+<td>Exif.Photo.ExposureIndex</td>
+<td>Rational</td>
+<td>Indicates the exposure index selected on the camera or input device at the time the image is captured.</td>
+</tr>
+<tr>
+<td>0xa217</td>
+<td>41495</td>
+<td>Photo</td>
+<td>Exif.Photo.SensingMethod</td>
+<td>Short</td>
+<td>Indicates the image sensor type on the camera or input device.</td>
+</tr>
+<tr>
+<td>0xa300</td>
+<td>41728</td>
+<td>Photo</td>
+<td>Exif.Photo.FileSource</td>
+<td>Undefined</td>
+<td>Indicates the image source. If a DSC recorded the image, this tag value of this tag always be set to 3, indicating that the image was recorded on a DSC.</td>
+</tr>
+<tr>
+<td>0xa301</td>
+<td>41729</td>
+<td>Photo</td>
+<td>Exif.Photo.SceneType</td>
+<td>Undefined</td>
+<td>Indicates the type of scene. If a DSC recorded the image, this tag value must always be set to 1, indicating that the image was directly photographed.</td>
+</tr>
+<tr>
+<td>0xa302</td>
+<td>41730</td>
+<td>Photo</td>
+<td>Exif.Photo.CFAPattern</td>
+<td>Undefined</td>
+<td>Indicates the color filter array (CFA) geometric pattern of the image sensor when a one-chip color area sensor is used. It does not apply to all sensing methods.</td>
+</tr>
+<tr>
+<td>0xa401</td>
+<td>41985</td>
+<td>Photo</td>
+<td>Exif.Photo.CustomRendered</td>
+<td>Short</td>
+<td>This tag indicates the use of special processing on image data, such as rendering geared to output. When special processing is performed, the reader is expected to disable or minimize any further processing.</td>
+</tr>
+<tr>
+<td>0xa402</td>
+<td>41986</td>
+<td>Photo</td>
+<td>Exif.Photo.ExposureMode</td>
+<td>Short</td>
+<td>This tag indicates the exposure mode set when the image was shot. In auto-bracketing mode, the camera shoots a series of frames of the same scene at different exposure settings.</td>
+</tr>
+<tr>
+<td>0xa403</td>
+<td>41987</td>
+<td>Photo</td>
+<td>Exif.Photo.WhiteBalance</td>
+<td>Short</td>
+<td>This tag indicates the white balance mode set when the image was shot.</td>
+</tr>
+<tr>
+<td>0xa404</td>
+<td>41988</td>
+<td>Photo</td>
+<td>Exif.Photo.DigitalZoomRatio</td>
+<td>Rational</td>
+<td>This tag indicates the digital zoom ratio when the image was shot. If the numerator of the recorded value is 0, this indicates that digital zoom was not used.</td>
+</tr>
+<tr>
+<td>0xa405</td>
+<td>41989</td>
+<td>Photo</td>
+<td>Exif.Photo.FocalLengthIn35mmFilm</td>
+<td>Short</td>
+<td>This tag indicates the equivalent focal length assuming a 35mm film camera, in mm. A value of 0 means the focal length is unknown. Note that this tag differs from the &lt;FocalLength&gt; tag.</td>
+</tr>
+<tr>
+<td>0xa406</td>
+<td>41990</td>
+<td>Photo</td>
+<td>Exif.Photo.SceneCaptureType</td>
+<td>Short</td>
+<td>This tag indicates the type of scene that was shot. It can also be used to record the mode in which the image was shot. Note that this differs from the &lt;SceneType&gt; tag.</td>
+</tr>
+<tr>
+<td>0xa407</td>
+<td>41991</td>
+<td>Photo</td>
+<td>Exif.Photo.GainControl</td>
+<td>Short</td>
+<td>This tag indicates the degree of overall image gain adjustment.</td>
+</tr>
+<tr>
+<td>0xa408</td>
+<td>41992</td>
+<td>Photo</td>
+<td>Exif.Photo.Contrast</td>
+<td>Short</td>
+<td>This tag indicates the direction of contrast processing applied by the camera when the image was shot.</td>
+</tr>
+<tr>
+<td>0xa409</td>
+<td>41993</td>
+<td>Photo</td>
+<td>Exif.Photo.Saturation</td>
+<td>Short</td>
+<td>This tag indicates the direction of saturation processing applied by the camera when the image was shot.</td>
+</tr>
+<tr>
+<td>0xa40a</td>
+<td>41994</td>
+<td>Photo</td>
+<td>Exif.Photo.Sharpness</td>
+<td>Short</td>
+<td>This tag indicates the direction of sharpness processing applied by the camera when the image was shot.</td>
+</tr>
+<tr>
+<td>0xa40b</td>
+<td>41995</td>
+<td>Photo</td>
+<td>Exif.Photo.DeviceSettingDescription</td>
+<td>Undefined</td>
+<td>This tag indicates information on the picture-taking conditions of a particular camera model. The tag is used only to indicate the picture-taking conditions in the reader.</td>
+</tr>
+<tr>
+<td>0xa40c</td>
+<td>41996</td>
+<td>Photo</td>
+<td>Exif.Photo.SubjectDistanceRange</td>
+<td>Short</td>
+<td>This tag indicates the distance to the subject.</td>
+</tr>
+<tr>
+<td>0xa420</td>
+<td>42016</td>
+<td>Photo</td>
+<td>Exif.Photo.ImageUniqueID</td>
+<td>Ascii</td>
+<td>This tag indicates an identifier assigned uniquely to each image. It is recorded as an ASCII string equivalent to hexadecimal notation and 128-bit fixed length.</td>
+</tr>
+<tr>
+<td>0xa430</td>
+<td>42032</td>
+<td>Photo</td>
+<td>Exif.Photo.CameraOwnerName</td>
+<td>Ascii</td>
+<td>This tag records the owner of a camera used in photography as an ASCII string.</td>
+</tr>
+<tr>
+<td>0xa431</td>
+<td>42033</td>
+<td>Photo</td>
+<td>Exif.Photo.BodySerialNumber</td>
+<td>Ascii</td>
+<td>This tag records the serial number of the body of the camera that was used in photography as an ASCII string.</td>
+</tr>
+<tr>
+<td>0xa432</td>
+<td>42034</td>
+<td>Photo</td>
+<td>Exif.Photo.LensSpecification</td>
+<td>Rational</td>
+<td>This tag notes minimum focal length, maximum focal length, minimum F number in the minimum focal length, and minimum F number in the maximum focal length, which are specification information for the lens that was used in photography. When the minimum F number is unknown, the notation is 0/0</td>
+</tr>
+<tr>
+<td>0xa433</td>
+<td>42035</td>
+<td>Photo</td>
+<td>Exif.Photo.LensMake</td>
+<td>Ascii</td>
+<td>This tag records the lens manufactor as an ASCII string.</td>
+</tr>
+<tr>
+<td>0xa434</td>
+<td>42036</td>
+<td>Photo</td>
+<td>Exif.Photo.LensModel</td>
+<td>Ascii</td>
+<td>This tag records the lens's model name and model number as an ASCII string.</td>
+</tr>
+<tr>
+<td>0xa435</td>
+<td>42037</td>
+<td>Photo</td>
+<td>Exif.Photo.LensSerialNumber</td>
+<td>Ascii</td>
+<td>This tag records the serial number of the interchangeable lens that was used in photography as an ASCII string.</td>
+</tr>
+<tr>
+<td>0x0001</td>
+<td>1</td>
+<td>Iop</td>
+<td>Exif.Iop.InteroperabilityIndex</td>
+<td>Ascii</td>
+<td>Indicates the identification of the Interoperability rule. Use "R98" for stating ExifR98 Rules. Four bytes used including the termination code (NULL). see the separate volume of Recommended Exif Interoperability Rules (ExifR98) for other tags used for ExifR98.</td>
+</tr>
+<tr>
+<td>0x0002</td>
+<td>2</td>
+<td>Iop</td>
+<td>Exif.Iop.InteroperabilityVersion</td>
+<td>Undefined</td>
+<td>Interoperability version</td>
+</tr>
+<tr>
+<td>0x1000</td>
+<td>4096</td>
+<td>Iop</td>
+<td>Exif.Iop.RelatedImageFileFormat</td>
+<td>Ascii</td>
+<td>File format of image file</td>
+</tr>
+<tr>
+<td>0x1001</td>
+<td>4097</td>
+<td>Iop</td>
+<td>Exif.Iop.RelatedImageWidth</td>
+<td>Long</td>
+<td>Image width</td>
+</tr>
+<tr>
+<td>0x1002</td>
+<td>4098</td>
+<td>Iop</td>
+<td>Exif.Iop.RelatedImageLength</td>
+<td>Long</td>
+<td>Image height</td>
+</tr>
+<tr>
+<td>0x0000</td>
+<td>0</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSVersionID</td>
+<td>Byte</td>
+<td>Indicates the version of &lt;GPSInfoIFD&gt;. The version is given as 2.0.0.0. This tag is mandatory when &lt;GPSInfo&gt; tag is present. (Note: The &lt;GPSVersionID&gt; tag is given in bytes, unlike the &lt;ExifVersion&gt; tag. When the version is 2.0.0.0, the tag value is 02000000.H).</td>
+</tr>
+<tr>
+<td>0x0001</td>
+<td>1</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSLatitudeRef</td>
+<td>Ascii</td>
+<td>Indicates whether the latitude is north or south latitude. The ASCII value 'N' indicates north latitude, and 'S' is south latitude.</td>
+</tr>
+<tr>
+<td>0x0002</td>
+<td>2</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSLatitude</td>
+<td>Rational</td>
+<td>Indicates the latitude. The latitude is expressed as three RATIONAL values giving the degrees, minutes, and seconds, respectively. When degrees, minutes and seconds are expressed, the format is dd/1,mm/1,ss/1. When degrees and minutes are used and, for example, fractions of minutes are given up to two decimal places, the format is dd/1,mmmm/100,0/1.</td>
+</tr>
+<tr>
+<td>0x0003</td>
+<td>3</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSLongitudeRef</td>
+<td>Ascii</td>
+<td>Indicates whether the longitude is east or west longitude. ASCII 'E' indicates east longitude, and 'W' is west longitude.</td>
+</tr>
+<tr>
+<td>0x0004</td>
+<td>4</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSLongitude</td>
+<td>Rational</td>
+<td>Indicates the longitude. The longitude is expressed as three RATIONAL values giving the degrees, minutes, and seconds, respectively. When degrees, minutes and seconds are expressed, the format is ddd/1,mm/1,ss/1. When degrees and minutes are used and, for example, fractions of minutes are given up to two decimal places, the format is ddd/1,mmmm/100,0/1.</td>
+</tr>
+<tr>
+<td>0x0005</td>
+<td>5</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSAltitudeRef</td>
+<td>Byte</td>
+<td>Indicates the altitude used as the reference altitude. If the reference is sea level and the altitude is above sea level, 0 is given. If the altitude is below sea level, a value of 1 is given and the altitude is indicated as an absolute value in the GSPAltitude tag. The reference unit is meters. Note that this tag is BYTE type, unlike other reference tags.</td>
+</tr>
+<tr>
+<td>0x0006</td>
+<td>6</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSAltitude</td>
+<td>Rational</td>
+<td>Indicates the altitude based on the reference in GPSAltitudeRef. Altitude is expressed as one RATIONAL value. The reference unit is meters.</td>
+</tr>
+<tr>
+<td>0x0007</td>
+<td>7</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSTimeStamp</td>
+<td>Rational</td>
+<td>Indicates the time as UTC (Coordinated Universal Time). &lt;TimeStamp&gt; is expressed as three RATIONAL values giving the hour, minute, and second (atomic clock).</td>
+</tr>
+<tr>
+<td>0x0008</td>
+<td>8</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSSatellites</td>
+<td>Ascii</td>
+<td>Indicates the GPS satellites used for measurements. This tag can be used to describe the number of satellites, their ID number, angle of elevation, azimuth, SNR and other information in ASCII notation. The format is not specified. If the GPS receiver is incapable of taking measurements, value of the tag is set to NULL.</td>
+</tr>
+<tr>
+<td>0x0009</td>
+<td>9</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSStatus</td>
+<td>Ascii</td>
+<td>Indicates the status of the GPS receiver when the image is recorded. "A" means measurement is in progress, and "V" means the measurement is Interoperability.</td>
+</tr>
+<tr>
+<td>0x000a</td>
+<td>10</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSMeasureMode</td>
+<td>Ascii</td>
+<td>Indicates the GPS measurement mode. "2" means two-dimensional measurement and "3" means three-dimensional measurement is in progress.</td>
+</tr>
+<tr>
+<td>0x000b</td>
+<td>11</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSDOP</td>
+<td>Rational</td>
+<td>Indicates the GPS DOP (data degree of precision). An HDOP value is written during two-dimensional measurement, and PDOP during three-dimensional measurement.</td>
+</tr>
+<tr>
+<td>0x000c</td>
+<td>12</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSSpeedRef</td>
+<td>Ascii</td>
+<td>Indicates the unit used to express the GPS receiver speed of movement. "K" "M" and "N" represents kilometers per hour, miles per hour, and knots.</td>
+</tr>
+<tr>
+<td>0x000d</td>
+<td>13</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSSpeed</td>
+<td>Rational</td>
+<td>Indicates the speed of GPS receiver movement.</td>
+</tr>
+<tr>
+<td>0x000e</td>
+<td>14</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSTrackRef</td>
+<td>Ascii</td>
+<td>Indicates the reference for giving the direction of GPS receiver movement. "T" denotes true direction and "M" is magnetic direction.</td>
+</tr>
+<tr>
+<td>0x000f</td>
+<td>15</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSTrack</td>
+<td>Rational</td>
+<td>Indicates the direction of GPS receiver movement. The range of values is from 0.00 to 359.99.</td>
+</tr>
+<tr>
+<td>0x0010</td>
+<td>16</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSImgDirectionRef</td>
+<td>Ascii</td>
+<td>Indicates the reference for giving the direction of the image when it is captured. "T" denotes true direction and "M" is magnetic direction.</td>
+</tr>
+<tr>
+<td>0x0011</td>
+<td>17</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSImgDirection</td>
+<td>Rational</td>
+<td>Indicates the direction of the image when it was captured. The range of values is from 0.00 to 359.99.</td>
+</tr>
+<tr>
+<td>0x0012</td>
+<td>18</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSMapDatum</td>
+<td>Ascii</td>
+<td>Indicates the geodetic survey data used by the GPS receiver. If the survey data is restricted to Japan, the value of this tag is "TOKYO" or "WGS-84".</td>
+</tr>
+<tr>
+<td>0x0013</td>
+<td>19</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSDestLatitudeRef</td>
+<td>Ascii</td>
+<td>Indicates whether the latitude of the destination point is north or south latitude. The ASCII value "N" indicates north latitude, and "S" is south latitude.</td>
+</tr>
+<tr>
+<td>0x0014</td>
+<td>20</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSDestLatitude</td>
+<td>Rational</td>
+<td>Indicates the latitude of the destination point. The latitude is expressed as three RATIONAL values giving the degrees, minutes, and seconds, respectively. If latitude is expressed as degrees, minutes and seconds, a typical format would be dd/1,mm/1,ss/1. When degrees and minutes are used and, for example, fractions of minutes are given up to two decimal places, the format would be dd/1,mmmm/100,0/1.</td>
+</tr>
+<tr>
+<td>0x0015</td>
+<td>21</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSDestLongitudeRef</td>
+<td>Ascii</td>
+<td>Indicates whether the longitude of the destination point is east or west longitude. ASCII "E" indicates east longitude, and "W" is west longitude.</td>
+</tr>
+<tr>
+<td>0x0016</td>
+<td>22</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSDestLongitude</td>
+<td>Rational</td>
+<td>Indicates the longitude of the destination point. The longitude is expressed as three RATIONAL values giving the degrees, minutes, and seconds, respectively. If longitude is expressed as degrees, minutes and seconds, a typical format would be ddd/1,mm/1,ss/1. When degrees and minutes are used and, for example, fractions of minutes are given up to two decimal places, the format would be ddd/1,mmmm/100,0/1.</td>
+</tr>
+<tr>
+<td>0x0017</td>
+<td>23</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSDestBearingRef</td>
+<td>Ascii</td>
+<td>Indicates the reference used for giving the bearing to the destination point. "T" denotes true direction and "M" is magnetic direction.</td>
+</tr>
+<tr>
+<td>0x0018</td>
+<td>24</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSDestBearing</td>
+<td>Rational</td>
+<td>Indicates the bearing to the destination point. The range of values is from 0.00 to 359.99.</td>
+</tr>
+<tr>
+<td>0x0019</td>
+<td>25</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSDestDistanceRef</td>
+<td>Ascii</td>
+<td>Indicates the unit used to express the distance to the destination point. "K", "M" and "N" represent kilometers, miles and knots.</td>
+</tr>
+<tr>
+<td>0x001a</td>
+<td>26</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSDestDistance</td>
+<td>Rational</td>
+<td>Indicates the distance to the destination point.</td>
+</tr>
+<tr>
+<td>0x001b</td>
+<td>27</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSProcessingMethod</td>
+<td>Undefined</td>
+<td>A character string recording the name of the method used for location finding. The first byte indicates the character code used, and this is followed by the name of the method.</td>
+</tr>
+<tr>
+<td>0x001c</td>
+<td>28</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSAreaInformation</td>
+<td>Undefined</td>
+<td>A character string recording the name of the GPS area. The first byte indicates the character code used, and this is followed by the name of the GPS area.</td>
+</tr>
+<tr>
+<td>0x001d</td>
+<td>29</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSDateStamp</td>
+<td>Ascii</td>
+<td>A character string recording date and time information relative to UTC (Coordinated Universal Time). The format is "YYYY:MM:DD.".</td>
+</tr>
+<tr>
+<td>0x001e</td>
+<td>30</td>
+<td>GPSInfo</td>
+<td>Exif.GPSInfo.GPSDifferential</td>
+<td>Short</td>
+<td>Indicates whether differential correction is applied to the GPS receiver.</td>
+</tr>
+</tbody>
+</table>
diff --git a/v2/assets/raw_tags/translate_tags.py b/v2/assets/raw_tags/translate_tags.py
new file mode 100755
index 0000000..77eba20
--- /dev/null
+++ b/v2/assets/raw_tags/translate_tags.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python2.7
+
+"""
+Parses the table-data from view-source:http://www.exiv2.org/tags.html
+"""
+
+import sys
+import collections
+
+import xml.etree.ElementTree as ET
+
+import ruamel.yaml
+
+
+# Prepare YAML to write hex expressions (otherwise the hex will be a string and
+# quotes or a decimal and a base-10 number).
+
+class HexInt(int):
+    pass
+
+def representer(dumper, data):
+    return \
+        ruamel.yaml.ScalarNode(
+            'tag:yaml.org,2002:int',
+            '0x{:04x}'.format(data))
+
+ruamel.yaml.add_representer(HexInt, representer)
+
+def _write(tags):
+    writeable = {}
+
+    for tag in tags:
+        pivot = tag['fq_key'].rindex('.')
+
+        item = {
+            'id': HexInt(tag['id_dec']),
+            'name': tag['fq_key'][pivot + 1:],
+            'type_name': tag['type'].upper(),
+        }
+
+        ifdName = tag['ifd']
+        if ifdName == 'Image':
+            ifdName = 'IFD'
+        if ifdName == 'Photo':
+            ifdName = 'Exif'
+
+        # UserComment. Has invalid type "COMMENT".
+        if item['id'] == 0x9286 and ifdName == 'Exif':
+            item['type_name'] = 'UNDEFINED'
+
+        try:
+            writeable[ifdName].append(item)
+        except KeyError:
+            writeable[ifdName] = [item]
+
+    with open('tags.yaml', 'w') as f:
+        # Otherwise, the next dictionaries will look like Python dictionaries,
+        # whatever sense that makes.
+        ruamel.yaml.dump(writeable, f, default_flow_style=False)
+
+def _main():
+    tree = ET.parse('tags.html')
+    root = tree.getroot()
+
+    labels = [
+        'id_hex',
+        'id_dec',
+        'ifd',
+        'fq_key',
+        'type',
+        'description',
+    ]
+
+    tags = []
+    for node in root.iter('tr'):
+        values = [child.text.strip() for child in node.iter('td')]
+
+        # Skips the header row.
+        if not values:
+            continue
+
+        assert \
+            len(values) == len(labels), \
+            "Row fields count not the same as labels: {}".format(values)
+
+        tags.append(dict(zip(labels, values)))
+
+    _write(tags)
+
+if __name__ == '__main__':
+    _main()
diff --git a/v2/assets/tags.yaml b/v2/assets/tags.yaml
new file mode 100644
index 0000000..080c919
--- /dev/null
+++ b/v2/assets/tags.yaml
@@ -0,0 +1,944 @@
+# Notes:
+#
+# This file was produced from http://www.exiv2.org/tags.html, using the included
+# tool, though that document appears to have some duplicates when all IDs are
+# supposed to be unique (EXIF information only has IDs, not IFDs; IFDs are
+# determined by our pre-existing knowledge of those tags).
+#
+# The webpage that we've produced this file from appears to indicate that
+# ImageWidth is represented by both 0x0100 and 0x0001 depending on whether the
+# encoding is RGB or YCbCr.
+Exif:
+- id: 0x829a
+  name: ExposureTime
+  type_name: RATIONAL
+- id: 0x829d
+  name: FNumber
+  type_name: RATIONAL
+- id: 0x8822
+  name: ExposureProgram
+  type_name: SHORT
+- id: 0x8824
+  name: SpectralSensitivity
+  type_name: ASCII
+- id: 0x8827
+  name: ISOSpeedRatings
+  type_name: SHORT
+- id: 0x8828
+  name: OECF
+  type_name: UNDEFINED
+- id: 0x8830
+  name: SensitivityType
+  type_name: SHORT
+- id: 0x8831
+  name: StandardOutputSensitivity
+  type_name: LONG
+- id: 0x8832
+  name: RecommendedExposureIndex
+  type_name: LONG
+- id: 0x8833
+  name: ISOSpeed
+  type_name: LONG
+- id: 0x8834
+  name: ISOSpeedLatitudeyyy
+  type_name: LONG
+- id: 0x8835
+  name: ISOSpeedLatitudezzz
+  type_name: LONG
+- id: 0x9000
+  name: ExifVersion
+  type_name: UNDEFINED
+- id: 0x9003
+  name: DateTimeOriginal
+  type_name: ASCII
+- id: 0x9004
+  name: DateTimeDigitized
+  type_name: ASCII
+- id: 0x9101
+  name: ComponentsConfiguration
+  type_name: UNDEFINED
+- id: 0x9102
+  name: CompressedBitsPerPixel
+  type_name: RATIONAL
+- id: 0x9201
+  name: ShutterSpeedValue
+  type_name: SRATIONAL
+- id: 0x9202
+  name: ApertureValue
+  type_name: RATIONAL
+- id: 0x9203
+  name: BrightnessValue
+  type_name: SRATIONAL
+- id: 0x9204
+  name: ExposureBiasValue
+  type_name: SRATIONAL
+- id: 0x9205
+  name: MaxApertureValue
+  type_name: RATIONAL
+- id: 0x9206
+  name: SubjectDistance
+  type_name: RATIONAL
+- id: 0x9207
+  name: MeteringMode
+  type_name: SHORT
+- id: 0x9208
+  name: LightSource
+  type_name: SHORT
+- id: 0x9209
+  name: Flash
+  type_name: SHORT
+- id: 0x920a
+  name: FocalLength
+  type_name: RATIONAL
+- id: 0x9214
+  name: SubjectArea
+  type_name: SHORT
+- id: 0x927c
+  name: MakerNote
+  type_name: UNDEFINED
+- id: 0x9286
+  name: UserComment
+  type_name: UNDEFINED
+- id: 0x9290
+  name: SubSecTime
+  type_name: ASCII
+- id: 0x9291
+  name: SubSecTimeOriginal
+  type_name: ASCII
+- id: 0x9292
+  name: SubSecTimeDigitized
+  type_name: ASCII
+- id: 0xa000
+  name: FlashpixVersion
+  type_name: UNDEFINED
+- id: 0xa001
+  name: ColorSpace
+  type_name: SHORT
+- id: 0xa002
+  name: PixelXDimension
+  type_name: LONG
+- id: 0xa003
+  name: PixelYDimension
+  type_name: LONG
+- id: 0xa004
+  name: RelatedSoundFile
+  type_name: ASCII
+- id: 0xa005
+  name: InteroperabilityTag
+  type_name: LONG
+- id: 0xa20b
+  name: FlashEnergy
+  type_name: RATIONAL
+- id: 0xa20c
+  name: SpatialFrequencyResponse
+  type_name: UNDEFINED
+- id: 0xa20e
+  name: FocalPlaneXResolution
+  type_name: RATIONAL
+- id: 0xa20f
+  name: FocalPlaneYResolution
+  type_name: RATIONAL
+- id: 0xa210
+  name: FocalPlaneResolutionUnit
+  type_name: SHORT
+- id: 0xa214
+  name: SubjectLocation
+  type_name: SHORT
+- id: 0xa215
+  name: ExposureIndex
+  type_name: RATIONAL
+- id: 0xa217
+  name: SensingMethod
+  type_name: SHORT
+- id: 0xa300
+  name: FileSource
+  type_name: UNDEFINED
+- id: 0xa301
+  name: SceneType
+  type_name: UNDEFINED
+- id: 0xa302
+  name: CFAPattern
+  type_name: UNDEFINED
+- id: 0xa401
+  name: CustomRendered
+  type_name: SHORT
+- id: 0xa402
+  name: ExposureMode
+  type_name: SHORT
+- id: 0xa403
+  name: WhiteBalance
+  type_name: SHORT
+- id: 0xa404
+  name: DigitalZoomRatio
+  type_name: RATIONAL
+- id: 0xa405
+  name: FocalLengthIn35mmFilm
+  type_name: SHORT
+- id: 0xa406
+  name: SceneCaptureType
+  type_name: SHORT
+- id: 0xa407
+  name: GainControl
+  type_name: SHORT
+- id: 0xa408
+  name: Contrast
+  type_name: SHORT
+- id: 0xa409
+  name: Saturation
+  type_name: SHORT
+- id: 0xa40a
+  name: Sharpness
+  type_name: SHORT
+- id: 0xa40b
+  name: DeviceSettingDescription
+  type_name: UNDEFINED
+- id: 0xa40c
+  name: SubjectDistanceRange
+  type_name: SHORT
+- id: 0xa420
+  name: ImageUniqueID
+  type_name: ASCII
+- id: 0xa430
+  name: CameraOwnerName
+  type_name: ASCII
+- id: 0xa431
+  name: BodySerialNumber
+  type_name: ASCII
+- id: 0xa432
+  name: LensSpecification
+  type_name: RATIONAL
+- id: 0xa433
+  name: LensMake
+  type_name: ASCII
+- id: 0xa434
+  name: LensModel
+  type_name: ASCII
+- id: 0xa435
+  name: LensSerialNumber
+  type_name: ASCII
+GPSInfo:
+- id: 0x0000
+  name: GPSVersionID
+  type_name: BYTE
+- id: 0x0001
+  name: GPSLatitudeRef
+  type_name: ASCII
+- id: 0x0002
+  name: GPSLatitude
+  type_name: RATIONAL
+- id: 0x0003
+  name: GPSLongitudeRef
+  type_name: ASCII
+- id: 0x0004
+  name: GPSLongitude
+  type_name: RATIONAL
+- id: 0x0005
+  name: GPSAltitudeRef
+  type_name: BYTE
+- id: 0x0006
+  name: GPSAltitude
+  type_name: RATIONAL
+- id: 0x0007
+  name: GPSTimeStamp
+  type_name: RATIONAL
+- id: 0x0008
+  name: GPSSatellites
+  type_name: ASCII
+- id: 0x0009
+  name: GPSStatus
+  type_name: ASCII
+- id: 0x000a
+  name: GPSMeasureMode
+  type_name: ASCII
+- id: 0x000b
+  name: GPSDOP
+  type_name: RATIONAL
+- id: 0x000c
+  name: GPSSpeedRef
+  type_name: ASCII
+- id: 0x000d
+  name: GPSSpeed
+  type_name: RATIONAL
+- id: 0x000e
+  name: GPSTrackRef
+  type_name: ASCII
+- id: 0x000f
+  name: GPSTrack
+  type_name: RATIONAL
+- id: 0x0010
+  name: GPSImgDirectionRef
+  type_name: ASCII
+- id: 0x0011
+  name: GPSImgDirection
+  type_name: RATIONAL
+- id: 0x0012
+  name: GPSMapDatum
+  type_name: ASCII
+- id: 0x0013
+  name: GPSDestLatitudeRef
+  type_name: ASCII
+- id: 0x0014
+  name: GPSDestLatitude
+  type_name: RATIONAL
+- id: 0x0015
+  name: GPSDestLongitudeRef
+  type_name: ASCII
+- id: 0x0016
+  name: GPSDestLongitude
+  type_name: RATIONAL
+- id: 0x0017
+  name: GPSDestBearingRef
+  type_name: ASCII
+- id: 0x0018
+  name: GPSDestBearing
+  type_name: RATIONAL
+- id: 0x0019
+  name: GPSDestDistanceRef
+  type_name: ASCII
+- id: 0x001a
+  name: GPSDestDistance
+  type_name: RATIONAL
+- id: 0x001b
+  name: GPSProcessingMethod
+  type_name: UNDEFINED
+- id: 0x001c
+  name: GPSAreaInformation
+  type_name: UNDEFINED
+- id: 0x001d
+  name: GPSDateStamp
+  type_name: ASCII
+- id: 0x001e
+  name: GPSDifferential
+  type_name: SHORT
+IFD:
+- id: 0x000b
+  name: ProcessingSoftware
+  type_name: ASCII
+- id: 0x00fe
+  name: NewSubfileType
+  type_name: LONG
+- id: 0x00ff
+  name: SubfileType
+  type_name: SHORT
+- id: 0x0100
+  name: ImageWidth
+  type_name: LONG
+- id: 0x0101
+  name: ImageLength
+  type_name: LONG
+- id: 0x0102
+  name: BitsPerSample
+  type_name: SHORT
+- id: 0x0103
+  name: Compression
+  type_name: SHORT
+- id: 0x0106
+  name: PhotometricInterpretation
+  type_name: SHORT
+- id: 0x0107
+  name: Thresholding
+  type_name: SHORT
+- id: 0x0108
+  name: CellWidth
+  type_name: SHORT
+- id: 0x0109
+  name: CellLength
+  type_name: SHORT
+- id: 0x010a
+  name: FillOrder
+  type_name: SHORT
+- id: 0x010d
+  name: DocumentName
+  type_name: ASCII
+- id: 0x010e
+  name: ImageDescription
+  type_name: ASCII
+- id: 0x010f
+  name: Make
+  type_name: ASCII
+- id: 0x0110
+  name: Model
+  type_name: ASCII
+- id: 0x0111
+  name: StripOffsets
+  type_name: LONG
+- id: 0x0112
+  name: Orientation
+  type_name: SHORT
+- id: 0x0115
+  name: SamplesPerPixel
+  type_name: SHORT
+- id: 0x0116
+  name: RowsPerStrip
+  type_name: LONG
+- id: 0x0117
+  name: StripByteCounts
+  type_name: LONG
+- id: 0x011a
+  name: XResolution
+  type_name: RATIONAL
+- id: 0x011b
+  name: YResolution
+  type_name: RATIONAL
+- id: 0x011c
+  name: PlanarConfiguration
+  type_name: SHORT
+- id: 0x0122
+  name: GrayResponseUnit
+  type_name: SHORT
+- id: 0x0123
+  name: GrayResponseCurve
+  type_name: SHORT
+- id: 0x0124
+  name: T4Options
+  type_name: LONG
+- id: 0x0125
+  name: T6Options
+  type_name: LONG
+- id: 0x0128
+  name: ResolutionUnit
+  type_name: SHORT
+- id: 0x0129
+  name: PageNumber
+  type_name: SHORT
+- id: 0x012d
+  name: TransferFunction
+  type_name: SHORT
+- id: 0x0131
+  name: Software
+  type_name: ASCII
+- id: 0x0132
+  name: DateTime
+  type_name: ASCII
+- id: 0x013b
+  name: Artist
+  type_name: ASCII
+- id: 0x013c
+  name: HostComputer
+  type_name: ASCII
+- id: 0x013d
+  name: Predictor
+  type_name: SHORT
+- id: 0x013e
+  name: WhitePoint
+  type_name: RATIONAL
+- id: 0x013f
+  name: PrimaryChromaticities
+  type_name: RATIONAL
+- id: 0x0140
+  name: ColorMap
+  type_name: SHORT
+- id: 0x0141
+  name: HalftoneHints
+  type_name: SHORT
+- id: 0x0142
+  name: TileWidth
+  type_name: SHORT
+- id: 0x0143
+  name: TileLength
+  type_name: SHORT
+- id: 0x0144
+  name: TileOffsets
+  type_name: SHORT
+- id: 0x0145
+  name: TileByteCounts
+  type_name: SHORT
+- id: 0x014a
+  name: SubIFDs
+  type_name: LONG
+- id: 0x014c
+  name: InkSet
+  type_name: SHORT
+- id: 0x014d
+  name: InkNames
+  type_name: ASCII
+- id: 0x014e
+  name: NumberOfInks
+  type_name: SHORT
+- id: 0x0150
+  name: DotRange
+  type_name: BYTE
+- id: 0x0151
+  name: TargetPrinter
+  type_name: ASCII
+- id: 0x0152
+  name: ExtraSamples
+  type_name: SHORT
+- id: 0x0153
+  name: SampleFormat
+  type_name: SHORT
+- id: 0x0154
+  name: SMinSampleValue
+  type_name: SHORT
+- id: 0x0155
+  name: SMaxSampleValue
+  type_name: SHORT
+- id: 0x0156
+  name: TransferRange
+  type_name: SHORT
+- id: 0x0157
+  name: ClipPath
+  type_name: BYTE
+- id: 0x0158
+  name: XClipPathUnits
+  type_name: SSHORT
+- id: 0x0159
+  name: YClipPathUnits
+  type_name: SSHORT
+- id: 0x015a
+  name: Indexed
+  type_name: SHORT
+- id: 0x015b
+  name: JPEGTables
+  type_name: UNDEFINED
+- id: 0x015f
+  name: OPIProxy
+  type_name: SHORT
+- id: 0x0200
+  name: JPEGProc
+  type_name: LONG
+- id: 0x0201
+  name: JPEGInterchangeFormat
+  type_name: LONG
+- id: 0x0202
+  name: JPEGInterchangeFormatLength
+  type_name: LONG
+- id: 0x0203
+  name: JPEGRestartInterval
+  type_name: SHORT
+- id: 0x0205
+  name: JPEGLosslessPredictors
+  type_name: SHORT
+- id: 0x0206
+  name: JPEGPointTransforms
+  type_name: SHORT
+- id: 0x0207
+  name: JPEGQTables
+  type_name: LONG
+- id: 0x0208
+  name: JPEGDCTables
+  type_name: LONG
+- id: 0x0209
+  name: JPEGACTables
+  type_name: LONG
+- id: 0x0211
+  name: YCbCrCoefficients
+  type_name: RATIONAL
+- id: 0x0212
+  name: YCbCrSubSampling
+  type_name: SHORT
+- id: 0x0213
+  name: YCbCrPositioning
+  type_name: SHORT
+- id: 0x0214
+  name: ReferenceBlackWhite
+  type_name: RATIONAL
+- id: 0x02bc
+  name: XMLPacket
+  type_name: BYTE
+- id: 0x4746
+  name: Rating
+  type_name: SHORT
+- id: 0x4749
+  name: RatingPercent
+  type_name: SHORT
+- id: 0x800d
+  name: ImageID
+  type_name: ASCII
+- id: 0x828d
+  name: CFARepeatPatternDim
+  type_name: SHORT
+- id: 0x828e
+  name: CFAPattern
+  type_name: BYTE
+- id: 0x828f
+  name: BatteryLevel
+  type_name: RATIONAL
+- id: 0x8298
+  name: Copyright
+  type_name: ASCII
+- id: 0x829a
+  name: ExposureTime
+  type_name: RATIONAL
+- id: 0x829d
+  name: FNumber
+  type_name: RATIONAL
+- id: 0x83bb
+  name: IPTCNAA
+  type_name: LONG
+- id: 0x8649
+  name: ImageResources
+  type_name: BYTE
+- id: 0x8769
+  name: ExifTag
+  type_name: LONG
+- id: 0x8773
+  name: InterColorProfile
+  type_name: UNDEFINED
+- id: 0x8822
+  name: ExposureProgram
+  type_name: SHORT
+- id: 0x8824
+  name: SpectralSensitivity
+  type_name: ASCII
+- id: 0x8825
+  name: GPSTag
+  type_name: LONG
+- id: 0x8827
+  name: ISOSpeedRatings
+  type_name: SHORT
+- id: 0x8828
+  name: OECF
+  type_name: UNDEFINED
+- id: 0x8829
+  name: Interlace
+  type_name: SHORT
+- id: 0x882a
+  name: TimeZoneOffset
+  type_name: SSHORT
+- id: 0x882b
+  name: SelfTimerMode
+  type_name: SHORT
+- id: 0x9003
+  name: DateTimeOriginal
+  type_name: ASCII
+- id: 0x9102
+  name: CompressedBitsPerPixel
+  type_name: RATIONAL
+- id: 0x9201
+  name: ShutterSpeedValue
+  type_name: SRATIONAL
+- id: 0x9202
+  name: ApertureValue
+  type_name: RATIONAL
+- id: 0x9203
+  name: BrightnessValue
+  type_name: SRATIONAL
+- id: 0x9204
+  name: ExposureBiasValue
+  type_name: SRATIONAL
+- id: 0x9205
+  name: MaxApertureValue
+  type_name: RATIONAL
+- id: 0x9206
+  name: SubjectDistance
+  type_name: SRATIONAL
+- id: 0x9207
+  name: MeteringMode
+  type_name: SHORT
+- id: 0x9208
+  name: LightSource
+  type_name: SHORT
+- id: 0x9209
+  name: Flash
+  type_name: SHORT
+- id: 0x920a
+  name: FocalLength
+  type_name: RATIONAL
+- id: 0x920b
+  name: FlashEnergy
+  type_name: RATIONAL
+- id: 0x920c
+  name: SpatialFrequencyResponse
+  type_name: UNDEFINED
+- id: 0x920d
+  name: Noise
+  type_name: UNDEFINED
+- id: 0x920e
+  name: FocalPlaneXResolution
+  type_name: RATIONAL
+- id: 0x920f
+  name: FocalPlaneYResolution
+  type_name: RATIONAL
+- id: 0x9210
+  name: FocalPlaneResolutionUnit
+  type_name: SHORT
+- id: 0x9211
+  name: ImageNumber
+  type_name: LONG
+- id: 0x9212
+  name: SecurityClassification
+  type_name: ASCII
+- id: 0x9213
+  name: ImageHistory
+  type_name: ASCII
+- id: 0x9214
+  name: SubjectLocation
+  type_name: SHORT
+- id: 0x9215
+  name: ExposureIndex
+  type_name: RATIONAL
+- id: 0x9216
+  name: TIFFEPStandardID
+  type_name: BYTE
+- id: 0x9217
+  name: SensingMethod
+  type_name: SHORT
+- id: 0x9c9b
+  name: XPTitle
+  type_name: BYTE
+- id: 0x9c9c
+  name: XPComment
+  type_name: BYTE
+- id: 0x9c9d
+  name: XPAuthor
+  type_name: BYTE
+- id: 0x9c9e
+  name: XPKeywords
+  type_name: BYTE
+- id: 0x9c9f
+  name: XPSubject
+  type_name: BYTE
+- id: 0xc4a5
+  name: PrintImageMatching
+  type_name: UNDEFINED
+- id: 0xc612
+  name: DNGVersion
+  type_name: BYTE
+- id: 0xc613
+  name: DNGBackwardVersion
+  type_name: BYTE
+- id: 0xc614
+  name: UniqueCameraModel
+  type_name: ASCII
+- id: 0xc615
+  name: LocalizedCameraModel
+  type_name: BYTE
+- id: 0xc616
+  name: CFAPlaneColor
+  type_name: BYTE
+- id: 0xc617
+  name: CFALayout
+  type_name: SHORT
+- id: 0xc618
+  name: LinearizationTable
+  type_name: SHORT
+- id: 0xc619
+  name: BlackLevelRepeatDim
+  type_name: SHORT
+- id: 0xc61a
+  name: BlackLevel
+  type_name: RATIONAL
+- id: 0xc61b
+  name: BlackLevelDeltaH
+  type_name: SRATIONAL
+- id: 0xc61c
+  name: BlackLevelDeltaV
+  type_name: SRATIONAL
+- id: 0xc61d
+  name: WhiteLevel
+  type_name: SHORT
+- id: 0xc61e
+  name: DefaultScale
+  type_name: RATIONAL
+- id: 0xc61f
+  name: DefaultCropOrigin
+  type_name: SHORT
+- id: 0xc620
+  name: DefaultCropSize
+  type_name: SHORT
+- id: 0xc621
+  name: ColorMatrix1
+  type_name: SRATIONAL
+- id: 0xc622
+  name: ColorMatrix2
+  type_name: SRATIONAL
+- id: 0xc623
+  name: CameraCalibration1
+  type_name: SRATIONAL
+- id: 0xc624
+  name: CameraCalibration2
+  type_name: SRATIONAL
+- id: 0xc625
+  name: ReductionMatrix1
+  type_name: SRATIONAL
+- id: 0xc626
+  name: ReductionMatrix2
+  type_name: SRATIONAL
+- id: 0xc627
+  name: AnalogBalance
+  type_name: RATIONAL
+- id: 0xc628
+  name: AsShotNeutral
+  type_name: SHORT
+- id: 0xc629
+  name: AsShotWhiteXY
+  type_name: RATIONAL
+- id: 0xc62a
+  name: BaselineExposure
+  type_name: SRATIONAL
+- id: 0xc62b
+  name: BaselineNoise
+  type_name: RATIONAL
+- id: 0xc62c
+  name: BaselineSharpness
+  type_name: RATIONAL
+- id: 0xc62d
+  name: BayerGreenSplit
+  type_name: LONG
+- id: 0xc62e
+  name: LinearResponseLimit
+  type_name: RATIONAL
+- id: 0xc62f
+  name: CameraSerialNumber
+  type_name: ASCII
+- id: 0xc630
+  name: LensInfo
+  type_name: RATIONAL
+- id: 0xc631
+  name: ChromaBlurRadius
+  type_name: RATIONAL
+- id: 0xc632
+  name: AntiAliasStrength
+  type_name: RATIONAL
+- id: 0xc633
+  name: ShadowScale
+  type_name: SRATIONAL
+- id: 0xc634
+  name: DNGPrivateData
+  type_name: BYTE
+- id: 0xc635
+  name: MakerNoteSafety
+  type_name: SHORT
+- id: 0xc65a
+  name: CalibrationIlluminant1
+  type_name: SHORT
+- id: 0xc65b
+  name: CalibrationIlluminant2
+  type_name: SHORT
+- id: 0xc65c
+  name: BestQualityScale
+  type_name: RATIONAL
+- id: 0xc65d
+  name: RawDataUniqueID
+  type_name: BYTE
+- id: 0xc68b
+  name: OriginalRawFileName
+  type_name: BYTE
+- id: 0xc68c
+  name: OriginalRawFileData
+  type_name: UNDEFINED
+- id: 0xc68d
+  name: ActiveArea
+  type_name: SHORT
+- id: 0xc68e
+  name: MaskedAreas
+  type_name: SHORT
+- id: 0xc68f
+  name: AsShotICCProfile
+  type_name: UNDEFINED
+- id: 0xc690
+  name: AsShotPreProfileMatrix
+  type_name: SRATIONAL
+- id: 0xc691
+  name: CurrentICCProfile
+  type_name: UNDEFINED
+- id: 0xc692
+  name: CurrentPreProfileMatrix
+  type_name: SRATIONAL
+- id: 0xc6bf
+  name: ColorimetricReference
+  type_name: SHORT
+- id: 0xc6f3
+  name: CameraCalibrationSignature
+  type_name: BYTE
+- id: 0xc6f4
+  name: ProfileCalibrationSignature
+  type_name: BYTE
+- id: 0xc6f6
+  name: AsShotProfileName
+  type_name: BYTE
+- id: 0xc6f7
+  name: NoiseReductionApplied
+  type_name: RATIONAL
+- id: 0xc6f8
+  name: ProfileName
+  type_name: BYTE
+- id: 0xc6f9
+  name: ProfileHueSatMapDims
+  type_name: LONG
+- id: 0xc6fa
+  name: ProfileHueSatMapData1
+  type_name: FLOAT
+- id: 0xc6fb
+  name: ProfileHueSatMapData2
+  type_name: FLOAT
+- id: 0xc6fc
+  name: ProfileToneCurve
+  type_name: FLOAT
+- id: 0xc6fd
+  name: ProfileEmbedPolicy
+  type_name: LONG
+- id: 0xc6fe
+  name: ProfileCopyright
+  type_name: BYTE
+- id: 0xc714
+  name: ForwardMatrix1
+  type_name: SRATIONAL
+- id: 0xc715
+  name: ForwardMatrix2
+  type_name: SRATIONAL
+- id: 0xc716
+  name: PreviewApplicationName
+  type_name: BYTE
+- id: 0xc717
+  name: PreviewApplicationVersion
+  type_name: BYTE
+- id: 0xc718
+  name: PreviewSettingsName
+  type_name: BYTE
+- id: 0xc719
+  name: PreviewSettingsDigest
+  type_name: BYTE
+- id: 0xc71a
+  name: PreviewColorSpace
+  type_name: LONG
+- id: 0xc71b
+  name: PreviewDateTime
+  type_name: ASCII
+- id: 0xc71c
+  name: RawImageDigest
+  type_name: UNDEFINED
+- id: 0xc71d
+  name: OriginalRawFileDigest
+  type_name: UNDEFINED
+- id: 0xc71e
+  name: SubTileBlockSize
+  type_name: LONG
+- id: 0xc71f
+  name: RowInterleaveFactor
+  type_name: LONG
+- id: 0xc725
+  name: ProfileLookTableDims
+  type_name: LONG
+- id: 0xc726
+  name: ProfileLookTableData
+  type_name: FLOAT
+- id: 0xc740
+  name: OpcodeList1
+  type_name: UNDEFINED
+- id: 0xc741
+  name: OpcodeList2
+  type_name: UNDEFINED
+- id: 0xc74e
+  name: OpcodeList3
+  type_name: UNDEFINED
+- id: 0xc761
+  name: NoiseProfile
+  type_name: DOUBLE
+Iop:
+- id: 0x0001
+  name: InteroperabilityIndex
+  type_name: ASCII
+- id: 0x0002
+  name: InteroperabilityVersion
+  type_name: UNDEFINED
+- id: 0x1000
+  name: RelatedImageFileFormat
+  type_name: ASCII
+- id: 0x1001
+  name: RelatedImageWidth
+  type_name: LONG
+- id: 0x1002
+  name: RelatedImageLength
+  type_name: LONG
diff --git a/v2/common_test.go b/v2/common_test.go
new file mode 100644
index 0000000..214429a
--- /dev/null
+++ b/v2/common_test.go
@@ -0,0 +1,187 @@
+package exif
+
+import (
+	"os"
+	"path"
+	"reflect"
+	"testing"
+
+	"io/ioutil"
+
+	"github.com/dsoprea/go-logging"
+)
+
+var (
+	assetsPath        = ""
+	testImageFilepath = ""
+
+	testExifData = make([]byte, 0)
+)
+
+func getExifSimpleTestIb() *IfdBuilder {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.Panic(err)
+		}
+	}()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	err = ib.AddStandard(0x000b, "asciivalue")
+	log.PanicIf(err)
+
+	err = ib.AddStandard(0x00ff, []uint16{0x1122})
+	log.PanicIf(err)
+
+	err = ib.AddStandard(0x0100, []uint32{0x33445566})
+	log.PanicIf(err)
+
+	err = ib.AddStandard(0x013e, []Rational{{Numerator: 0x11112222, Denominator: 0x33334444}})
+	log.PanicIf(err)
+
+	return ib
+}
+
+func getExifSimpleTestIbBytes() []byte {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.Panic(err)
+		}
+	}()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	err = ib.AddStandard(0x000b, "asciivalue")
+	log.PanicIf(err)
+
+	err = ib.AddStandard(0x00ff, []uint16{0x1122})
+	log.PanicIf(err)
+
+	err = ib.AddStandard(0x0100, []uint32{0x33445566})
+	log.PanicIf(err)
+
+	err = ib.AddStandard(0x013e, []Rational{{Numerator: 0x11112222, Denominator: 0x33334444}})
+	log.PanicIf(err)
+
+	ibe := NewIfdByteEncoder()
+
+	exifData, err := ibe.EncodeToExif(ib)
+	log.PanicIf(err)
+
+	return exifData
+}
+
+func validateExifSimpleTestIb(exifData []byte, t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.Panic(err)
+		}
+	}()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	eh, index, err := Collect(im, ti, exifData)
+	log.PanicIf(err)
+
+	if eh.ByteOrder != TestDefaultByteOrder {
+		t.Fatalf("EXIF byte-order is not correct: %v", eh.ByteOrder)
+	} else if eh.FirstIfdOffset != ExifDefaultFirstIfdOffset {
+		t.Fatalf("EXIF first IFD-offset not correct: (0x%02x)", eh.FirstIfdOffset)
+	}
+
+	if len(index.Ifds) != 1 {
+		t.Fatalf("There wasn't exactly one IFD decoded: (%d)", len(index.Ifds))
+	}
+
+	ifd := index.RootIfd
+
+	if ifd.ByteOrder != TestDefaultByteOrder {
+		t.Fatalf("IFD byte-order not correct.")
+	} else if ifd.IfdPath != IfdStandard {
+		t.Fatalf("IFD name not correct.")
+	} else if ifd.Index != 0 {
+		t.Fatalf("IFD index not zero: (%d)", ifd.Index)
+	} else if ifd.Offset != uint32(0x0008) {
+		t.Fatalf("IFD offset not correct.")
+	} else if len(ifd.Entries) != 4 {
+		t.Fatalf("IFD number of entries not correct: (%d)", len(ifd.Entries))
+	} else if ifd.NextIfdOffset != uint32(0) {
+		t.Fatalf("Next-IFD offset is non-zero.")
+	} else if ifd.NextIfd != nil {
+		t.Fatalf("Next-IFD pointer is non-nil.")
+	}
+
+	// Verify the values by using the actual, orginal types (this is awesome).
+
+	addressableData := exifData[ExifAddressableAreaStart:]
+
+	expected := []struct {
+		tagId uint16
+		value interface{}
+	}{
+		{tagId: 0x000b, value: "asciivalue"},
+		{tagId: 0x00ff, value: []uint16{0x1122}},
+		{tagId: 0x0100, value: []uint32{0x33445566}},
+		{tagId: 0x013e, value: []Rational{{Numerator: 0x11112222, Denominator: 0x33334444}}},
+	}
+
+	for i, e := range ifd.Entries {
+		if e.TagId != expected[i].tagId {
+			t.Fatalf("Tag-ID for entry (%d) not correct: (0x%02x) != (0x%02x)", i, e.TagId, expected[i].tagId)
+		}
+
+		value, err := e.Value(addressableData, TestDefaultByteOrder)
+		log.PanicIf(err)
+
+		if reflect.DeepEqual(value, expected[i].value) != true {
+			t.Fatalf("Value for entry (%d) not correct: [%v] != [%v]", i, value, expected[i].value)
+		}
+	}
+}
+
+func init() {
+	// This will only be executed when we're running tests in this package and
+	// not when this package is being imported from a subpackage.
+
+	goPath := os.Getenv("GOPATH")
+	if goPath != "" {
+		assetsPath = path.Join(goPath, "src", "github.com", "dsoprea", "go-exif", "assets")
+	} else {
+		// Module-enabled context.
+
+		currentWd, err := os.Getwd()
+		log.PanicIf(err)
+
+		assetsPath = path.Join(currentWd, "assets")
+	}
+
+	testImageFilepath = path.Join(assetsPath, "NDM_8901.jpg")
+
+	// Load test EXIF data.
+
+	filepath := path.Join(assetsPath, "NDM_8901.jpg.exif")
+
+	var err error
+	testExifData, err = ioutil.ReadFile(filepath)
+	log.PanicIf(err)
+}
diff --git a/v2/error.go b/v2/error.go
new file mode 100644
index 0000000..dc9ebe5
--- /dev/null
+++ b/v2/error.go
@@ -0,0 +1,10 @@
+package exif
+
+import (
+    "errors"
+)
+
+var (
+    ErrTagNotFound = errors.New("tag not found")
+    ErrTagNotStandard = errors.New("tag not a standard tag")
+)
diff --git a/v2/exif-read-tool/main.go b/v2/exif-read-tool/main.go
new file mode 100644
index 0000000..b5ef20e
--- /dev/null
+++ b/v2/exif-read-tool/main.go
@@ -0,0 +1,160 @@
+// This tool dumps EXIF information from images.
+//
+// Example command-line:
+//
+//   exif-read-tool -filepath <file-path>
+//
+// Example Output:
+//
+//   IFD=[IfdIdentity<PARENT-NAME=[] NAME=[IFD]>] ID=(0x010f) NAME=[Make] COUNT=(6) TYPE=[ASCII] VALUE=[Canon]
+//   IFD=[IfdIdentity<PARENT-NAME=[] NAME=[IFD]>] ID=(0x0110) NAME=[Model] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III]
+//   IFD=[IfdIdentity<PARENT-NAME=[] NAME=[IFD]>] ID=(0x0112) NAME=[Orientation] COUNT=(1) TYPE=[SHORT] VALUE=[1]
+//   IFD=[IfdIdentity<PARENT-NAME=[] NAME=[IFD]>] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]
+//   ...
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+
+	"encoding/json"
+	"io/ioutil"
+
+	"github.com/dsoprea/go-exif"
+	"github.com/dsoprea/go-logging"
+)
+
+var (
+	filepathArg     = ""
+	printAsJsonArg  = false
+	printLoggingArg = false
+)
+
+type IfdEntry struct {
+	IfdPath     string                `json:"ifd_path"`
+	FqIfdPath   string                `json:"fq_ifd_path"`
+	IfdIndex    int                   `json:"ifd_index"`
+	TagId       uint16                `json:"tag_id"`
+	TagName     string                `json:"tag_name"`
+	TagTypeId   exif.TagTypePrimitive `json:"tag_type_id"`
+	TagTypeName string                `json:"tag_type_name"`
+	UnitCount   uint32                `json:"unit_count"`
+	Value       interface{}           `json:"value"`
+	ValueString string                `json:"value_string"`
+}
+
+func main() {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Program error.")
+			os.Exit(1)
+		}
+	}()
+
+	flag.StringVar(&filepathArg, "filepath", "", "File-path of image")
+	flag.BoolVar(&printAsJsonArg, "json", false, "Print JSON")
+	flag.BoolVar(&printLoggingArg, "verbose", false, "Print logging")
+
+	flag.Parse()
+
+	if filepathArg == "" {
+		fmt.Printf("Please provide a file-path for an image.\n")
+		os.Exit(1)
+	}
+
+	if printLoggingArg == true {
+		cla := log.NewConsoleLogAdapter()
+		log.AddAdapter("console", cla)
+	}
+
+	f, err := os.Open(filepathArg)
+	log.PanicIf(err)
+
+	data, err := ioutil.ReadAll(f)
+	log.PanicIf(err)
+
+	rawExif, err := exif.SearchAndExtractExif(data)
+	log.PanicIf(err)
+
+	// Run the parse.
+
+	im := exif.NewIfdMappingWithStandard()
+	ti := exif.NewTagIndex()
+
+	entries := make([]IfdEntry, 0)
+	visitor := func(fqIfdPath string, ifdIndex int, tagId uint16, tagType exif.TagType, valueContext exif.ValueContext) (err error) {
+		defer func() {
+			if state := recover(); state != nil {
+				err = log.Wrap(state.(error))
+				log.Panic(err)
+			}
+		}()
+
+		ifdPath, err := im.StripPathPhraseIndices(fqIfdPath)
+		log.PanicIf(err)
+
+		it, err := ti.Get(ifdPath, tagId)
+		if err != nil {
+			if log.Is(err, exif.ErrTagNotFound) {
+				fmt.Printf("WARNING: Unknown tag: [%s] (%04x)\n", ifdPath, tagId)
+				return nil
+			} else {
+				log.Panic(err)
+			}
+		}
+
+		valueString := ""
+		var value interface{}
+		if tagType.Type() == exif.TypeUndefined {
+			var err error
+			value, err = valueContext.Undefined()
+			if err != nil {
+				if err == exif.ErrUnhandledUnknownTypedTag {
+					value = nil
+				} else {
+					log.Panic(err)
+				}
+			}
+
+			valueString = fmt.Sprintf("%v", value)
+		} else {
+			valueString, err = valueContext.FormatFirst()
+			log.PanicIf(err)
+
+			value = valueString
+		}
+
+		entry := IfdEntry{
+			IfdPath:     ifdPath,
+			FqIfdPath:   fqIfdPath,
+			IfdIndex:    ifdIndex,
+			TagId:       tagId,
+			TagName:     it.Name,
+			TagTypeId:   tagType.Type(),
+			TagTypeName: tagType.Name(),
+			UnitCount:   valueContext.UnitCount(),
+			Value:       value,
+			ValueString: valueString,
+		}
+
+		entries = append(entries, entry)
+
+		return nil
+	}
+
+	_, err = exif.Visit(exif.IfdStandard, im, ti, rawExif, visitor)
+	log.PanicIf(err)
+
+	if printAsJsonArg == true {
+		data, err := json.MarshalIndent(entries, "", "    ")
+		log.PanicIf(err)
+
+		fmt.Println(string(data))
+	} else {
+		for _, entry := range entries {
+			fmt.Printf("IFD-PATH=[%s] ID=(0x%04x) NAME=[%s] COUNT=(%d) TYPE=[%s] VALUE=[%s]\n", entry.IfdPath, entry.TagId, entry.TagName, entry.UnitCount, entry.TagTypeName, entry.ValueString)
+		}
+	}
+}
diff --git a/v2/exif-read-tool/main_test.go b/v2/exif-read-tool/main_test.go
new file mode 100644
index 0000000..b2ac0a7
--- /dev/null
+++ b/v2/exif-read-tool/main_test.go
@@ -0,0 +1,166 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"path"
+	"reflect"
+	"testing"
+
+	"encoding/json"
+	"io/ioutil"
+	"os/exec"
+
+	"github.com/dsoprea/go-logging"
+)
+
+var (
+	assetsPath        = ""
+	appFilepath       = ""
+	testImageFilepath = ""
+)
+
+func TestMain(t *testing.T) {
+	cmd := exec.Command(
+		"go", "run", appFilepath,
+		"-filepath", testImageFilepath)
+
+	b := new(bytes.Buffer)
+	cmd.Stdout = b
+	cmd.Stderr = b
+
+	err := cmd.Run()
+	actual := b.String()
+
+	if err != nil {
+		fmt.Printf(actual)
+		log.Panic(err)
+	}
+
+	expected := `IFD-PATH=[IFD] ID=(0x010f) NAME=[Make] COUNT=(6) TYPE=[ASCII] VALUE=[Canon]
+IFD-PATH=[IFD] ID=(0x0110) NAME=[Model] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III]
+IFD-PATH=[IFD] ID=(0x0112) NAME=[Orientation] COUNT=(1) TYPE=[SHORT] VALUE=[1]
+IFD-PATH=[IFD] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]
+IFD-PATH=[IFD] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]
+IFD-PATH=[IFD] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]
+IFD-PATH=[IFD] ID=(0x0132) NAME=[DateTime] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]
+IFD-PATH=[IFD] ID=(0x013b) NAME=[Artist] COUNT=(1) TYPE=[ASCII] VALUE=[]
+IFD-PATH=[IFD] ID=(0x0213) NAME=[YCbCrPositioning] COUNT=(1) TYPE=[SHORT] VALUE=[2]
+IFD-PATH=[IFD] ID=(0x8298) NAME=[Copyright] COUNT=(1) TYPE=[ASCII] VALUE=[]
+IFD-PATH=[IFD] ID=(0x8769) NAME=[ExifTag] COUNT=(1) TYPE=[LONG] VALUE=[360]
+IFD-PATH=[IFD/Exif] ID=(0x829a) NAME=[ExposureTime] COUNT=(1) TYPE=[RATIONAL] VALUE=[1/640]
+IFD-PATH=[IFD/Exif] ID=(0x829d) NAME=[FNumber] COUNT=(1) TYPE=[RATIONAL] VALUE=[4/1]
+IFD-PATH=[IFD/Exif] ID=(0x8822) NAME=[ExposureProgram] COUNT=(1) TYPE=[SHORT] VALUE=[4]
+IFD-PATH=[IFD/Exif] ID=(0x8827) NAME=[ISOSpeedRatings] COUNT=(1) TYPE=[SHORT] VALUE=[1600]
+IFD-PATH=[IFD/Exif] ID=(0x8830) NAME=[SensitivityType] COUNT=(1) TYPE=[SHORT] VALUE=[2]
+IFD-PATH=[IFD/Exif] ID=(0x8832) NAME=[RecommendedExposureIndex] COUNT=(1) TYPE=[LONG] VALUE=[1600]
+IFD-PATH=[IFD/Exif] ID=(0x9000) NAME=[ExifVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0230]
+IFD-PATH=[IFD/Exif] ID=(0x9003) NAME=[DateTimeOriginal] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]
+IFD-PATH=[IFD/Exif] ID=(0x9004) NAME=[DateTimeDigitized] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]
+IFD-PATH=[IFD/Exif] ID=(0x9101) NAME=[ComponentsConfiguration] COUNT=(4) TYPE=[UNDEFINED] VALUE=[ComponentsConfiguration<ID=[YCBCR] BYTES=[1 2 3 0]>]
+IFD-PATH=[IFD/Exif] ID=(0x9201) NAME=[ShutterSpeedValue] COUNT=(1) TYPE=[SRATIONAL] VALUE=[614400/65536]
+IFD-PATH=[IFD/Exif] ID=(0x9202) NAME=[ApertureValue] COUNT=(1) TYPE=[RATIONAL] VALUE=[262144/65536]
+IFD-PATH=[IFD/Exif] ID=(0x9204) NAME=[ExposureBiasValue] COUNT=(1) TYPE=[SRATIONAL] VALUE=[0/1]
+IFD-PATH=[IFD/Exif] ID=(0x9207) NAME=[MeteringMode] COUNT=(1) TYPE=[SHORT] VALUE=[5]
+IFD-PATH=[IFD/Exif] ID=(0x9209) NAME=[Flash] COUNT=(1) TYPE=[SHORT] VALUE=[16]
+IFD-PATH=[IFD/Exif] ID=(0x920a) NAME=[FocalLength] COUNT=(1) TYPE=[RATIONAL] VALUE=[16/1]
+IFD-PATH=[IFD/Exif] ID=(0x927c) NAME=[MakerNote] COUNT=(8152) TYPE=[UNDEFINED] VALUE=[MakerNote<TYPE-ID=[28 00 01 00 03 00 31 00 00 00 74 05 00 00 02 00 03 00 04 00] LEN=(8152) SHA1=[d4154aa7df5474efe7ab38de2595919b9b4cc29f]>]
+IFD-PATH=[IFD/Exif] ID=(0x9286) NAME=[UserComment] COUNT=(264) TYPE=[UNDEFINED] VALUE=[UserComment<SIZE=(256) ENCODING=[UNDEFINED] V=[0 0 0 0 0 0 0 0]... LEN=(256)>]
+IFD-PATH=[IFD/Exif] ID=(0x9290) NAME=[SubSecTime] COUNT=(3) TYPE=[ASCII] VALUE=[00]
+IFD-PATH=[IFD/Exif] ID=(0x9291) NAME=[SubSecTimeOriginal] COUNT=(3) TYPE=[ASCII] VALUE=[00]
+IFD-PATH=[IFD/Exif] ID=(0x9292) NAME=[SubSecTimeDigitized] COUNT=(3) TYPE=[ASCII] VALUE=[00]
+IFD-PATH=[IFD/Exif] ID=(0xa000) NAME=[FlashpixVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0100]
+IFD-PATH=[IFD/Exif] ID=(0xa001) NAME=[ColorSpace] COUNT=(1) TYPE=[SHORT] VALUE=[1]
+IFD-PATH=[IFD/Exif] ID=(0xa002) NAME=[PixelXDimension] COUNT=(1) TYPE=[SHORT] VALUE=[3840]
+IFD-PATH=[IFD/Exif] ID=(0xa003) NAME=[PixelYDimension] COUNT=(1) TYPE=[SHORT] VALUE=[2560]
+IFD-PATH=[IFD/Exif] ID=(0xa005) NAME=[InteroperabilityTag] COUNT=(1) TYPE=[LONG] VALUE=[9326]
+IFD-PATH=[IFD/Exif/Iop] ID=(0x0001) NAME=[InteroperabilityIndex] COUNT=(4) TYPE=[ASCII] VALUE=[R98]
+IFD-PATH=[IFD/Exif/Iop] ID=(0x0002) NAME=[InteroperabilityVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0100]
+IFD-PATH=[IFD/Exif] ID=(0xa20e) NAME=[FocalPlaneXResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[3840000/1461]
+IFD-PATH=[IFD/Exif] ID=(0xa20f) NAME=[FocalPlaneYResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[2560000/972]
+IFD-PATH=[IFD/Exif] ID=(0xa210) NAME=[FocalPlaneResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]
+IFD-PATH=[IFD/Exif] ID=(0xa401) NAME=[CustomRendered] COUNT=(1) TYPE=[SHORT] VALUE=[0]
+IFD-PATH=[IFD/Exif] ID=(0xa402) NAME=[ExposureMode] COUNT=(1) TYPE=[SHORT] VALUE=[0]
+IFD-PATH=[IFD/Exif] ID=(0xa403) NAME=[WhiteBalance] COUNT=(1) TYPE=[SHORT] VALUE=[0]
+IFD-PATH=[IFD/Exif] ID=(0xa406) NAME=[SceneCaptureType] COUNT=(1) TYPE=[SHORT] VALUE=[0]
+IFD-PATH=[IFD/Exif] ID=(0xa430) NAME=[CameraOwnerName] COUNT=(1) TYPE=[ASCII] VALUE=[]
+IFD-PATH=[IFD/Exif] ID=(0xa431) NAME=[BodySerialNumber] COUNT=(13) TYPE=[ASCII] VALUE=[063024020097]
+IFD-PATH=[IFD/Exif] ID=(0xa432) NAME=[LensSpecification] COUNT=(4) TYPE=[RATIONAL] VALUE=[16/1...]
+IFD-PATH=[IFD/Exif] ID=(0xa434) NAME=[LensModel] COUNT=(22) TYPE=[ASCII] VALUE=[EF16-35mm f/4L IS USM]
+IFD-PATH=[IFD/Exif] ID=(0xa435) NAME=[LensSerialNumber] COUNT=(11) TYPE=[ASCII] VALUE=[2400001068]
+IFD-PATH=[IFD] ID=(0x8825) NAME=[GPSTag] COUNT=(1) TYPE=[LONG] VALUE=[9554]
+IFD-PATH=[IFD/GPSInfo] ID=(0x0000) NAME=[GPSVersionID] COUNT=(4) TYPE=[BYTE] VALUE=[02 03 00 00]
+IFD-PATH=[IFD] ID=(0x0103) NAME=[Compression] COUNT=(1) TYPE=[SHORT] VALUE=[6]
+IFD-PATH=[IFD] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]
+IFD-PATH=[IFD] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]
+IFD-PATH=[IFD] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]
+`
+
+	if actual != expected {
+		t.Fatalf("Output not as expected:\nACTUAL:\n%s\nEXPECTED:\n%s", actual, expected)
+	}
+}
+
+func TestMainJson(t *testing.T) {
+	cmd := exec.Command(
+		"go", "run", appFilepath,
+		"-filepath", testImageFilepath,
+		"-json")
+
+	b := new(bytes.Buffer)
+	cmd.Stdout = b
+	cmd.Stderr = b
+
+	err := cmd.Run()
+	actualRaw := b.Bytes()
+
+	if err != nil {
+		fmt.Printf(string(actualRaw))
+		log.Panic(err)
+	}
+
+	// Parse actual data.
+
+	actual := make([]map[string]interface{}, 0)
+
+	err = json.Unmarshal(actualRaw, &actual)
+	log.PanicIf(err)
+
+	// Read and parse expected data.
+
+	jsonFilepath := path.Join(assetsPath, "exif_read.json")
+
+	expectedRaw, err := ioutil.ReadFile(jsonFilepath)
+	log.PanicIf(err)
+
+	expected := make([]map[string]interface{}, 0)
+
+	err = json.Unmarshal(expectedRaw, &expected)
+	log.PanicIf(err)
+
+	if reflect.DeepEqual(actual, expected) == false {
+		t.Fatalf("Output not as expected:\nACTUAL:\n%s\nEXPECTED:\n%s", actualRaw, expectedRaw)
+	}
+}
+
+func init() {
+	goPath := os.Getenv("GOPATH")
+
+	moduleRoot := ""
+	if goPath != "" {
+		moduleRoot = path.Join(goPath, "src", "github.com", "dsoprea", "go-exif")
+	} else {
+		// Module-enabled context.
+
+		currentWd, err := os.Getwd()
+		log.PanicIf(err)
+
+		moduleRoot = path.Join(currentWd, "..")
+	}
+
+	assetsPath = path.Join(moduleRoot, "assets")
+	appFilepath = path.Join(moduleRoot, "exif-read-tool", "main.go")
+
+	testImageFilepath = path.Join(assetsPath, "NDM_8901.jpg")
+}
diff --git a/v2/exif.go b/v2/exif.go
new file mode 100644
index 0000000..ae40f01
--- /dev/null
+++ b/v2/exif.go
@@ -0,0 +1,247 @@
+package exif
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"os"
+
+	"encoding/binary"
+	"io/ioutil"
+
+	"github.com/dsoprea/go-logging"
+)
+
+const (
+	// ExifAddressableAreaStart is the absolute offset in the file that all
+	// offsets are relative to.
+	ExifAddressableAreaStart = uint32(0x0)
+
+	// ExifDefaultFirstIfdOffset is essentially the number of bytes in addition
+	// to `ExifAddressableAreaStart` that you have to move in order to escape
+	// the rest of the header and get to the earliest point where we can put
+	// stuff (which has to be the first IFD). This is the size of the header
+	// sequence containing the two-character byte-order, two-character fixed-
+	// bytes, and the four bytes describing the first-IFD offset.
+	ExifDefaultFirstIfdOffset = uint32(2 + 2 + 4)
+)
+
+var (
+	exifLogger = log.NewLogger("exif.exif")
+
+	// EncodeDefaultByteOrder is the default byte-order for encoding operations.
+	EncodeDefaultByteOrder = binary.BigEndian
+
+	// Default byte order for tests.
+	TestDefaultByteOrder = binary.BigEndian
+
+	BigEndianBoBytes    = [2]byte{'M', 'M'}
+	LittleEndianBoBytes = [2]byte{'I', 'I'}
+
+	ByteOrderLookup = map[[2]byte]binary.ByteOrder{
+		BigEndianBoBytes:    binary.BigEndian,
+		LittleEndianBoBytes: binary.LittleEndian,
+	}
+
+	ByteOrderLookupR = map[binary.ByteOrder][2]byte{
+		binary.BigEndian:    BigEndianBoBytes,
+		binary.LittleEndian: LittleEndianBoBytes,
+	}
+
+	ExifFixedBytesLookup = map[binary.ByteOrder][2]byte{
+		binary.LittleEndian: [2]byte{0x2a, 0x00},
+		binary.BigEndian:    [2]byte{0x00, 0x2a},
+	}
+)
+
+var (
+	ErrNoExif          = errors.New("no exif data")
+	ErrExifHeaderError = errors.New("exif header error")
+)
+
+// SearchAndExtractExif returns a slice from the beginning of the EXIF data to
+// end of the file (it's not practical to try and calculate where the data
+// actually ends; it needs to be formally parsed).
+func SearchAndExtractExif(data []byte) (rawExif []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.Panic(err)
+		}
+	}()
+
+	// Search for the beginning of the EXIF information. The EXIF is near the
+	// beginning of our/most JPEGs, so this has a very low cost.
+
+	foundAt := -1
+	for i := 0; i < len(data); i++ {
+		if _, err := ParseExifHeader(data[i:]); err == nil {
+			foundAt = i
+			break
+		} else if log.Is(err, ErrNoExif) == false {
+			return nil, err
+		}
+	}
+
+	if foundAt == -1 {
+		return nil, ErrNoExif
+	}
+
+	return data[foundAt:], nil
+}
+
+// SearchFileAndExtractExif returns a slice from the beginning of the EXIF data
+// to the end of the file (it's not practical to try and calculate where the
+// data actually ends).
+func SearchFileAndExtractExif(filepath string) (rawExif []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.Panic(err)
+		}
+	}()
+
+	// Open the file.
+
+	f, err := os.Open(filepath)
+	log.PanicIf(err)
+
+	defer f.Close()
+
+	data, err := ioutil.ReadAll(f)
+	log.PanicIf(err)
+
+	rawExif, err = SearchAndExtractExif(data)
+	log.PanicIf(err)
+
+	return rawExif, nil
+}
+
+type ExifHeader struct {
+	ByteOrder      binary.ByteOrder
+	FirstIfdOffset uint32
+}
+
+func (eh ExifHeader) String() string {
+	return fmt.Sprintf("ExifHeader<BYTE-ORDER=[%v] FIRST-IFD-OFFSET=(0x%02x)>", eh.ByteOrder, eh.FirstIfdOffset)
+}
+
+// ParseExifHeader parses the bytes at the very top of the header.
+//
+// This will panic with ErrNoExif on any data errors so that we can double as
+// an EXIF-detection routine.
+func ParseExifHeader(data []byte) (eh ExifHeader, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// Good reference:
+	//
+	//      CIPA DC-008-2016; JEITA CP-3451D
+	//      -> http://www.cipa.jp/std/documents/e/DC-008-Translation-2016-E.pdf
+
+	if len(data) < 2 {
+		exifLogger.Warningf(nil, "Not enough data for EXIF header (1): (%d)", len(data))
+		return eh, ErrNoExif
+	}
+
+	byteOrderBytes := [2]byte{data[0], data[1]}
+
+	byteOrder, found := ByteOrderLookup[byteOrderBytes]
+	if found == false {
+		// exifLogger.Warningf(nil, "EXIF byte-order not recognized: [%v]", byteOrderBytes)
+		return eh, ErrNoExif
+	}
+
+	if len(data) < 4 {
+		exifLogger.Warningf(nil, "Not enough data for EXIF header (2): (%d)", len(data))
+		return eh, ErrNoExif
+	}
+
+	fixedBytes := [2]byte{data[2], data[3]}
+	expectedFixedBytes := ExifFixedBytesLookup[byteOrder]
+	if fixedBytes != expectedFixedBytes {
+		// exifLogger.Warningf(nil, "EXIF header fixed-bytes should be [%v] but are: [%v]", expectedFixedBytes, fixedBytes)
+		return eh, ErrNoExif
+	}
+
+	if len(data) < 2 {
+		exifLogger.Warningf(nil, "Not enough data for EXIF header (3): (%d)", len(data))
+		return eh, ErrNoExif
+	}
+
+	firstIfdOffset := byteOrder.Uint32(data[4:8])
+
+	eh = ExifHeader{
+		ByteOrder:      byteOrder,
+		FirstIfdOffset: firstIfdOffset,
+	}
+
+	return eh, nil
+}
+
+// Visit recursively invokes a callback for every tag.
+func Visit(rootIfdName string, ifdMapping *IfdMapping, tagIndex *TagIndex, exifData []byte, visitor RawTagVisitor) (eh ExifHeader, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	eh, err = ParseExifHeader(exifData)
+	log.PanicIf(err)
+
+	ie := NewIfdEnumerate(ifdMapping, tagIndex, exifData, eh.ByteOrder)
+
+	err = ie.Scan(rootIfdName, eh.FirstIfdOffset, visitor, true)
+	log.PanicIf(err)
+
+	return eh, nil
+}
+
+// Collect recursively builds a static structure of all IFDs and tags.
+func Collect(ifdMapping *IfdMapping, tagIndex *TagIndex, exifData []byte) (eh ExifHeader, index IfdIndex, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	eh, err = ParseExifHeader(exifData)
+	log.PanicIf(err)
+
+	ie := NewIfdEnumerate(ifdMapping, tagIndex, exifData, eh.ByteOrder)
+
+	index, err = ie.Collect(eh.FirstIfdOffset, true)
+	log.PanicIf(err)
+
+	return eh, index, nil
+}
+
+// BuildExifHeader constructs the bytes that go in the very beginning.
+func BuildExifHeader(byteOrder binary.ByteOrder, firstIfdOffset uint32) (headerBytes []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	b := new(bytes.Buffer)
+
+	// This is the point in the data that all offsets are relative to.
+	boBytes := ByteOrderLookupR[byteOrder]
+	_, err = b.WriteString(string(boBytes[:]))
+	log.PanicIf(err)
+
+	fixedBytes := ExifFixedBytesLookup[byteOrder]
+
+	_, err = b.Write(fixedBytes[:])
+	log.PanicIf(err)
+
+	err = binary.Write(b, byteOrder, firstIfdOffset)
+	log.PanicIf(err)
+
+	return b.Bytes(), nil
+}
diff --git a/v2/exif_test.go b/v2/exif_test.go
new file mode 100644
index 0000000..a67d472
--- /dev/null
+++ b/v2/exif_test.go
@@ -0,0 +1,368 @@
+package exif
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"reflect"
+	"testing"
+
+	"encoding/binary"
+	"io/ioutil"
+
+	"github.com/dsoprea/go-logging"
+)
+
+func TestVisit(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Exif failure.")
+		}
+	}()
+
+	ti := NewTagIndex()
+
+	// Open the file.
+
+	f, err := os.Open(testImageFilepath)
+	log.PanicIf(err)
+
+	defer f.Close()
+
+	data, err := ioutil.ReadAll(f)
+	log.PanicIf(err)
+
+	// Search for the beginning of the EXIF information. The EXIF is near the
+	// very beginning of our/most JPEGs, so this has a very low cost.
+
+	foundAt := -1
+	for i := 0; i < len(data); i++ {
+		if _, err := ParseExifHeader(data[i:]); err == nil {
+			foundAt = i
+			break
+		} else if log.Is(err, ErrNoExif) == false {
+			log.Panic(err)
+		}
+	}
+
+	if foundAt == -1 {
+		log.Panicf("EXIF start not found")
+	}
+
+	// Run the parse.
+
+	im := NewIfdMappingWithStandard()
+
+	tags := make([]string, 0)
+
+	visitor := func(fqIfdPath string, ifdIndex int, tagId uint16, tagType TagType, valueContext ValueContext) (err error) {
+		defer func() {
+			if state := recover(); state != nil {
+				err = log.Wrap(state.(error))
+				log.Panic(err)
+			}
+		}()
+
+		ifdPath, err := im.StripPathPhraseIndices(fqIfdPath)
+		log.PanicIf(err)
+
+		it, err := ti.Get(ifdPath, tagId)
+		if err != nil {
+			if log.Is(err, ErrTagNotFound) {
+				fmt.Printf("Unknown tag: [%s] (%04x)\n", ifdPath, tagId)
+				return nil
+			} else {
+				log.Panic(err)
+			}
+		}
+
+		valueString := ""
+		if tagType.Type() == TypeUndefined {
+			value, err := valueContext.Undefined()
+			if err != nil {
+				if err == ErrUnhandledUnknownTypedTag {
+					valueString = "!UNDEFINED!"
+				} else {
+					log.Panic(err)
+				}
+			}
+
+			valueString = fmt.Sprintf("%v", value)
+		} else {
+			valueString, err = valueContext.FormatFirst()
+			log.PanicIf(err)
+		}
+
+		description := fmt.Sprintf("IFD-PATH=[%s] ID=(0x%04x) NAME=[%s] COUNT=(%d) TYPE=[%s] VALUE=[%s]", ifdPath, tagId, it.Name, valueContext.UnitCount(), tagType.Name(), valueString)
+		tags = append(tags, description)
+
+		return nil
+	}
+
+	_, err = Visit(IfdStandard, im, ti, data[foundAt:], visitor)
+	log.PanicIf(err)
+
+	expected := []string{
+		"IFD-PATH=[IFD] ID=(0x010f) NAME=[Make] COUNT=(6) TYPE=[ASCII] VALUE=[Canon]",
+		"IFD-PATH=[IFD] ID=(0x0110) NAME=[Model] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III]",
+		"IFD-PATH=[IFD] ID=(0x0112) NAME=[Orientation] COUNT=(1) TYPE=[SHORT] VALUE=[1]",
+		"IFD-PATH=[IFD] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
+		"IFD-PATH=[IFD] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
+		"IFD-PATH=[IFD] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
+		"IFD-PATH=[IFD] ID=(0x0132) NAME=[DateTime] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]",
+		"IFD-PATH=[IFD] ID=(0x013b) NAME=[Artist] COUNT=(1) TYPE=[ASCII] VALUE=[]",
+		"IFD-PATH=[IFD] ID=(0x0213) NAME=[YCbCrPositioning] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
+		"IFD-PATH=[IFD] ID=(0x8298) NAME=[Copyright] COUNT=(1) TYPE=[ASCII] VALUE=[]",
+		"IFD-PATH=[IFD] ID=(0x8769) NAME=[ExifTag] COUNT=(1) TYPE=[LONG] VALUE=[360]",
+		"IFD-PATH=[IFD/Exif] ID=(0x829a) NAME=[ExposureTime] COUNT=(1) TYPE=[RATIONAL] VALUE=[1/640]",
+		"IFD-PATH=[IFD/Exif] ID=(0x829d) NAME=[FNumber] COUNT=(1) TYPE=[RATIONAL] VALUE=[4/1]",
+		"IFD-PATH=[IFD/Exif] ID=(0x8822) NAME=[ExposureProgram] COUNT=(1) TYPE=[SHORT] VALUE=[4]",
+		"IFD-PATH=[IFD/Exif] ID=(0x8827) NAME=[ISOSpeedRatings] COUNT=(1) TYPE=[SHORT] VALUE=[1600]",
+		"IFD-PATH=[IFD/Exif] ID=(0x8830) NAME=[SensitivityType] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
+		"IFD-PATH=[IFD/Exif] ID=(0x8832) NAME=[RecommendedExposureIndex] COUNT=(1) TYPE=[LONG] VALUE=[1600]",
+		"IFD-PATH=[IFD/Exif] ID=(0x9000) NAME=[ExifVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0230]",
+		"IFD-PATH=[IFD/Exif] ID=(0x9003) NAME=[DateTimeOriginal] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]",
+		"IFD-PATH=[IFD/Exif] ID=(0x9004) NAME=[DateTimeDigitized] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]",
+		"IFD-PATH=[IFD/Exif] ID=(0x9101) NAME=[ComponentsConfiguration] COUNT=(4) TYPE=[UNDEFINED] VALUE=[ComponentsConfiguration<ID=[YCBCR] BYTES=[1 2 3 0]>]",
+		"IFD-PATH=[IFD/Exif] ID=(0x9201) NAME=[ShutterSpeedValue] COUNT=(1) TYPE=[SRATIONAL] VALUE=[614400/65536]",
+		"IFD-PATH=[IFD/Exif] ID=(0x9202) NAME=[ApertureValue] COUNT=(1) TYPE=[RATIONAL] VALUE=[262144/65536]",
+		"IFD-PATH=[IFD/Exif] ID=(0x9204) NAME=[ExposureBiasValue] COUNT=(1) TYPE=[SRATIONAL] VALUE=[0/1]",
+		"IFD-PATH=[IFD/Exif] ID=(0x9207) NAME=[MeteringMode] COUNT=(1) TYPE=[SHORT] VALUE=[5]",
+		"IFD-PATH=[IFD/Exif] ID=(0x9209) NAME=[Flash] COUNT=(1) TYPE=[SHORT] VALUE=[16]",
+		"IFD-PATH=[IFD/Exif] ID=(0x920a) NAME=[FocalLength] COUNT=(1) TYPE=[RATIONAL] VALUE=[16/1]",
+		"IFD-PATH=[IFD/Exif] ID=(0x927c) NAME=[MakerNote] COUNT=(8152) TYPE=[UNDEFINED] VALUE=[MakerNote<TYPE-ID=[28 00 01 00 03 00 31 00 00 00 74 05 00 00 02 00 03 00 04 00] LEN=(8152) SHA1=[d4154aa7df5474efe7ab38de2595919b9b4cc29f]>]",
+		"IFD-PATH=[IFD/Exif] ID=(0x9286) NAME=[UserComment] COUNT=(264) TYPE=[UNDEFINED] VALUE=[UserComment<SIZE=(256) ENCODING=[UNDEFINED] V=[0 0 0 0 0 0 0 0]... LEN=(256)>]",
+		"IFD-PATH=[IFD/Exif] ID=(0x9290) NAME=[SubSecTime] COUNT=(3) TYPE=[ASCII] VALUE=[00]",
+		"IFD-PATH=[IFD/Exif] ID=(0x9291) NAME=[SubSecTimeOriginal] COUNT=(3) TYPE=[ASCII] VALUE=[00]",
+		"IFD-PATH=[IFD/Exif] ID=(0x9292) NAME=[SubSecTimeDigitized] COUNT=(3) TYPE=[ASCII] VALUE=[00]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa000) NAME=[FlashpixVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0100]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa001) NAME=[ColorSpace] COUNT=(1) TYPE=[SHORT] VALUE=[1]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa002) NAME=[PixelXDimension] COUNT=(1) TYPE=[SHORT] VALUE=[3840]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa003) NAME=[PixelYDimension] COUNT=(1) TYPE=[SHORT] VALUE=[2560]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa005) NAME=[InteroperabilityTag] COUNT=(1) TYPE=[LONG] VALUE=[9326]",
+		"IFD-PATH=[IFD/Exif/Iop] ID=(0x0001) NAME=[InteroperabilityIndex] COUNT=(4) TYPE=[ASCII] VALUE=[R98]",
+		"IFD-PATH=[IFD/Exif/Iop] ID=(0x0002) NAME=[InteroperabilityVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0100]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa20e) NAME=[FocalPlaneXResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[3840000/1461]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa20f) NAME=[FocalPlaneYResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[2560000/972]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa210) NAME=[FocalPlaneResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa401) NAME=[CustomRendered] COUNT=(1) TYPE=[SHORT] VALUE=[0]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa402) NAME=[ExposureMode] COUNT=(1) TYPE=[SHORT] VALUE=[0]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa403) NAME=[WhiteBalance] COUNT=(1) TYPE=[SHORT] VALUE=[0]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa406) NAME=[SceneCaptureType] COUNT=(1) TYPE=[SHORT] VALUE=[0]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa430) NAME=[CameraOwnerName] COUNT=(1) TYPE=[ASCII] VALUE=[]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa431) NAME=[BodySerialNumber] COUNT=(13) TYPE=[ASCII] VALUE=[063024020097]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa432) NAME=[LensSpecification] COUNT=(4) TYPE=[RATIONAL] VALUE=[16/1...]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa434) NAME=[LensModel] COUNT=(22) TYPE=[ASCII] VALUE=[EF16-35mm f/4L IS USM]",
+		"IFD-PATH=[IFD/Exif] ID=(0xa435) NAME=[LensSerialNumber] COUNT=(11) TYPE=[ASCII] VALUE=[2400001068]",
+		"IFD-PATH=[IFD] ID=(0x8825) NAME=[GPSTag] COUNT=(1) TYPE=[LONG] VALUE=[9554]",
+		"IFD-PATH=[IFD/GPSInfo] ID=(0x0000) NAME=[GPSVersionID] COUNT=(4) TYPE=[BYTE] VALUE=[02 03 00 00]",
+		"IFD-PATH=[IFD] ID=(0x0103) NAME=[Compression] COUNT=(1) TYPE=[SHORT] VALUE=[6]",
+		"IFD-PATH=[IFD] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
+		"IFD-PATH=[IFD] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
+		"IFD-PATH=[IFD] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
+	}
+
+	if reflect.DeepEqual(tags, expected) == false {
+		fmt.Printf("\n")
+		fmt.Printf("ACTUAL:\n")
+		fmt.Printf("\n")
+
+		for _, line := range tags {
+			fmt.Println(line)
+		}
+
+		fmt.Printf("\n")
+		fmt.Printf("EXPECTED:\n")
+		fmt.Printf("\n")
+
+		for _, line := range expected {
+			fmt.Println(line)
+		}
+
+		fmt.Printf("\n")
+
+		t.Fatalf("tags not correct.")
+	}
+}
+
+func TestSearchFileAndExtractExif(t *testing.T) {
+	// Returns a slice starting with the EXIF data and going to the end of the
+	// image.
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	if bytes.Compare(rawExif[:len(testExifData)], testExifData) != 0 {
+		t.Fatalf("found EXIF data not correct")
+	}
+}
+
+func TestSearchAndExtractExif(t *testing.T) {
+	imageData, err := ioutil.ReadFile(testImageFilepath)
+	log.PanicIf(err)
+
+	rawExif, err := SearchAndExtractExif(imageData)
+	log.PanicIf(err)
+
+	if bytes.Compare(rawExif[:len(testExifData)], testExifData) != 0 {
+		t.Fatalf("found EXIF data not correct")
+	}
+}
+
+func TestCollect(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Exif failure.")
+		}
+	}()
+
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	rootIfd := index.RootIfd
+	ifds := index.Ifds
+	tree := index.Tree
+	lookup := index.Lookup
+
+	if rootIfd.Offset != uint32(0x0008) {
+		t.Fatalf("Root-IFD not correct: (0x%04d).", rootIfd.Offset)
+	} else if rootIfd.Id != 0 {
+		t.Fatalf("Root-IFD does not have the right ID: (%d)", rootIfd.Id)
+	} else if tree[0] != rootIfd {
+		t.Fatalf("Root-IFD is not indexed properly.")
+	} else if len(ifds) != 5 {
+		t.Fatalf("The IFD list is not the right size: (%d)", len(ifds))
+	} else if len(tree) != 5 {
+		t.Fatalf("The IFD tree is not the right size: (%d)", len(tree))
+	} else if len(lookup) != 4 {
+		t.Fatalf("The IFD lookup is not the right size: (%d)", len(lookup))
+	}
+
+	if rootIfd.NextIfdOffset != 0x2c54 {
+		t.Fatalf("Root IFD does not continue correctly: (0x%04x)", rootIfd.NextIfdOffset)
+	} else if rootIfd.NextIfd.Offset != rootIfd.NextIfdOffset {
+		t.Fatalf("Root IFD neighbor object does not have the right offset: (0x%04x != 0x%04x)", rootIfd.NextIfd.Offset, rootIfd.NextIfdOffset)
+	} else if rootIfd.NextIfd.NextIfdOffset != 0 {
+		t.Fatalf("Root IFD chain not terminated correctly (1).")
+	} else if rootIfd.NextIfd.NextIfd != nil {
+		t.Fatalf("Root IFD chain not terminated correctly (2).")
+	}
+
+	if rootIfd.IfdPath != IfdPathStandard {
+		t.Fatalf("Root IFD is not labeled correctly: [%s]", rootIfd.IfdPath)
+	} else if rootIfd.NextIfd.IfdPath != IfdPathStandard {
+		t.Fatalf("Root IFD sibling is not labeled correctly: [%s]", rootIfd.IfdPath)
+	} else if rootIfd.Children[0].IfdPath != IfdPathStandardExif {
+		t.Fatalf("Root IFD child (0) is not labeled correctly: [%s]", rootIfd.Children[0].IfdPath)
+	} else if rootIfd.Children[1].IfdPath != IfdPathStandardGps {
+		t.Fatalf("Root IFD child (1) is not labeled correctly: [%s]", rootIfd.Children[1].IfdPath)
+	} else if rootIfd.Children[0].Children[0].IfdPath != IfdPathStandardExifIop {
+		t.Fatalf("Exif IFD child is not an IOP IFD: [%s]", rootIfd.Children[0].Children[0].IfdPath)
+	}
+
+	if lookup[IfdPathStandard][0].IfdPath != IfdPathStandard {
+		t.Fatalf("Lookup for standard IFD not correct.")
+	} else if lookup[IfdPathStandard][1].IfdPath != IfdPathStandard {
+		t.Fatalf("Lookup for standard IFD not correct.")
+	}
+
+	if lookup[IfdPathStandardExif][0].IfdPath != IfdPathStandardExif {
+		t.Fatalf("Lookup for EXIF IFD not correct.")
+	}
+
+	if lookup[IfdPathStandardGps][0].IfdPath != IfdPathStandardGps {
+		t.Fatalf("Lookup for GPS IFD not correct.")
+	}
+
+	if lookup[IfdPathStandardExifIop][0].IfdPath != IfdPathStandardExifIop {
+		t.Fatalf("Lookup for IOP IFD not correct.")
+	}
+
+	foundExif := 0
+	foundGps := 0
+	for _, ite := range lookup[IfdPathStandard][0].Entries {
+		if ite.ChildIfdPath == IfdPathStandardExif {
+			foundExif++
+
+			if ite.TagId != IfdExifId {
+				t.Fatalf("EXIF IFD tag-ID mismatch: (0x%04x) != (0x%04x)", ite.TagId, IfdExifId)
+			}
+		}
+
+		if ite.ChildIfdPath == IfdPathStandardGps {
+			foundGps++
+
+			if ite.TagId != IfdGpsId {
+				t.Fatalf("GPS IFD tag-ID mismatch: (0x%04x) != (0x%04x)", ite.TagId, IfdGpsId)
+			}
+		}
+	}
+
+	if foundExif != 1 {
+		t.Fatalf("Exactly one EXIF IFD tag wasn't found: (%d)", foundExif)
+	} else if foundGps != 1 {
+		t.Fatalf("Exactly one GPS IFD tag wasn't found: (%d)", foundGps)
+	}
+
+	foundIop := 0
+	for _, ite := range lookup[IfdPathStandardExif][0].Entries {
+		if ite.ChildIfdPath == IfdPathStandardExifIop {
+			foundIop++
+
+			if ite.TagId != IfdIopId {
+				t.Fatalf("IOP IFD tag-ID mismatch: (0x%04x) != (0x%04x)", ite.TagId, IfdIopId)
+			}
+		}
+	}
+
+	if foundIop != 1 {
+		t.Fatalf("Exactly one IOP IFD tag wasn't found: (%d)", foundIop)
+	}
+}
+
+func TestParseExifHeader(t *testing.T) {
+	eh, err := ParseExifHeader(testExifData)
+	log.PanicIf(err)
+
+	if eh.ByteOrder != binary.LittleEndian {
+		t.Fatalf("Byte-order of EXIF header not correct.")
+	} else if eh.FirstIfdOffset != 0x8 {
+		t.Fatalf("First IFD offset not correct.")
+	}
+}
+
+func TestExif_BuildAndParseExifHeader(t *testing.T) {
+	headerBytes, err := BuildExifHeader(TestDefaultByteOrder, 0x11223344)
+	log.PanicIf(err)
+
+	eh, err := ParseExifHeader(headerBytes)
+	log.PanicIf(err)
+
+	if eh.ByteOrder != TestDefaultByteOrder {
+		t.Fatalf("Byte-order of EXIF header not correct.")
+	} else if eh.FirstIfdOffset != 0x11223344 {
+		t.Fatalf("First IFD offset not correct.")
+	}
+}
+
+func ExampleBuildExifHeader() {
+	headerBytes, err := BuildExifHeader(TestDefaultByteOrder, 0x11223344)
+	log.PanicIf(err)
+
+	eh, err := ParseExifHeader(headerBytes)
+	log.PanicIf(err)
+
+	fmt.Printf("%v\n", eh)
+	// Output: ExifHeader<BYTE-ORDER=[BigEndian] FIRST-IFD-OFFSET=(0x11223344)>
+}
diff --git a/v2/go.mod b/v2/go.mod
new file mode 100644
index 0000000..e19afe6
--- /dev/null
+++ b/v2/go.mod
@@ -0,0 +1,11 @@
+module github.com/dsoprea/go-exif/v2
+
+go 1.13
+
+require (
+	github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696
+	github.com/go-errors/errors v1.0.1 // indirect
+	github.com/golang/geo v0.0.0-20190916061304-5b978397cfec
+	golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
+	gopkg.in/yaml.v2 v2.2.7
+)
diff --git a/v2/go.sum b/v2/go.sum
new file mode 100644
index 0000000..a36fc59
--- /dev/null
+++ b/v2/go.sum
@@ -0,0 +1,14 @@
+github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696 h1:VGFnZAcLwPpt1sHlAxml+pGLZz9A2s+K/s1YNhPC91Y=
+github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
+github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
+github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
+github.com/golang/geo v0.0.0-20190916061304-5b978397cfec h1:lJwO/92dFXWeXOZdoGXgptLmNLwynMSHUmU6besqtiw=
+github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
+gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/v2/gps.go b/v2/gps.go
new file mode 100644
index 0000000..7d74f22
--- /dev/null
+++ b/v2/gps.go
@@ -0,0 +1,56 @@
+package exif
+
+import (
+	"errors"
+	"fmt"
+	"time"
+
+	"github.com/golang/geo/s2"
+)
+
+var (
+	ErrGpsCoordinatesNotValid = errors.New("GPS coordinates not valid")
+)
+
+type GpsDegrees struct {
+	Orientation               byte
+	Degrees, Minutes, Seconds float64
+}
+
+func (d GpsDegrees) String() string {
+	return fmt.Sprintf("Degrees<O=[%s] D=(%g) M=(%g) S=(%g)>", string([]byte{d.Orientation}), d.Degrees, d.Minutes, d.Seconds)
+}
+
+func (d GpsDegrees) Decimal() float64 {
+	decimal := float64(d.Degrees) + float64(d.Minutes)/60.0 + float64(d.Seconds)/3600.0
+
+	if d.Orientation == 'S' || d.Orientation == 'W' {
+		return -decimal
+	} else {
+		return decimal
+	}
+}
+
+type GpsInfo struct {
+	Latitude, Longitude GpsDegrees
+	Altitude            int
+	Timestamp           time.Time
+}
+
+func (gi *GpsInfo) String() string {
+	return fmt.Sprintf("GpsInfo<LAT=(%.05f) LON=(%.05f) ALT=(%d) TIME=[%s]>", gi.Latitude.Decimal(), gi.Longitude.Decimal(), gi.Altitude, gi.Timestamp)
+}
+
+func (gi *GpsInfo) S2CellId() s2.CellID {
+	latitude := gi.Latitude.Decimal()
+	longitude := gi.Longitude.Decimal()
+
+	ll := s2.LatLngFromDegrees(latitude, longitude)
+	cellId := s2.CellIDFromLatLng(ll)
+
+	if cellId.IsValid() == false {
+		panic(ErrGpsCoordinatesNotValid)
+	}
+
+	return cellId
+}
diff --git a/v2/ifd.go b/v2/ifd.go
new file mode 100644
index 0000000..e75404d
--- /dev/null
+++ b/v2/ifd.go
@@ -0,0 +1,407 @@
+package exif
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/dsoprea/go-logging"
+)
+
+const (
+	// IFD names. The paths that we referred to the IFDs with are comprised of
+	// these.
+
+	IfdStandard = "IFD"
+	IfdExif     = "Exif"
+	IfdGps      = "GPSInfo"
+	IfdIop      = "Iop"
+
+	// Tag IDs for child IFDs.
+
+	IfdExifId = 0x8769
+	IfdGpsId  = 0x8825
+	IfdIopId  = 0xA005
+
+	// Just a placeholder.
+
+	IfdRootId = 0x0000
+
+	// The paths of the standard IFDs expressed in the standard IFD-mappings
+	// and as the group-names in the tag data.
+
+	IfdPathStandard        = "IFD"
+	IfdPathStandardExif    = "IFD/Exif"
+	IfdPathStandardExifIop = "IFD/Exif/Iop"
+	IfdPathStandardGps     = "IFD/GPSInfo"
+)
+
+var (
+	ifdLogger = log.NewLogger("exif.ifd")
+)
+
+var (
+	ErrChildIfdNotMapped = errors.New("no child-IFD for that tag-ID under parent")
+)
+
+// type IfdIdentity struct {
+// 	ParentIfdName string
+// 	IfdName       string
+// }
+
+// func (ii IfdIdentity) String() string {
+// 	return fmt.Sprintf("IfdIdentity<PARENT-NAME=[%s] NAME=[%s]>", ii.ParentIfdName, ii.IfdName)
+// }
+
+type MappedIfd struct {
+	ParentTagId uint16
+	Placement   []uint16
+	Path        []string
+
+	Name     string
+	TagId    uint16
+	Children map[uint16]*MappedIfd
+}
+
+func (mi *MappedIfd) String() string {
+	pathPhrase := mi.PathPhrase()
+	return fmt.Sprintf("MappedIfd<(0x%04X) [%s] PATH=[%s]>", mi.TagId, mi.Name, pathPhrase)
+}
+
+func (mi *MappedIfd) PathPhrase() string {
+	return strings.Join(mi.Path, "/")
+}
+
+// IfdMapping describes all of the IFDs that we currently recognize.
+type IfdMapping struct {
+	rootNode *MappedIfd
+}
+
+func NewIfdMapping() (ifdMapping *IfdMapping) {
+	rootNode := &MappedIfd{
+		Path:     make([]string, 0),
+		Children: make(map[uint16]*MappedIfd),
+	}
+
+	return &IfdMapping{
+		rootNode: rootNode,
+	}
+}
+
+func NewIfdMappingWithStandard() (ifdMapping *IfdMapping) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.Panic(err)
+		}
+	}()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	return im
+}
+
+func (im *IfdMapping) Get(parentPlacement []uint16) (childIfd *MappedIfd, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	ptr := im.rootNode
+	for _, tagId := range parentPlacement {
+		if descendantPtr, found := ptr.Children[tagId]; found == false {
+			log.Panicf("ifd child with tag-ID (%04x) not registered: [%s]", tagId, ptr.PathPhrase())
+		} else {
+			ptr = descendantPtr
+		}
+	}
+
+	return ptr, nil
+}
+
+func (im *IfdMapping) GetWithPath(pathPhrase string) (mi *MappedIfd, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	if pathPhrase == "" {
+		log.Panicf("path-phrase is empty")
+	}
+
+	path := strings.Split(pathPhrase, "/")
+	ptr := im.rootNode
+
+	for _, name := range path {
+		var hit *MappedIfd
+		for _, mi := range ptr.Children {
+			if mi.Name == name {
+				hit = mi
+				break
+			}
+		}
+
+		if hit == nil {
+			log.Panicf("ifd child with name [%s] not registered: [%s]", name, ptr.PathPhrase())
+		}
+
+		ptr = hit
+	}
+
+	return ptr, nil
+}
+
+// GetChild is a convenience function to get the child path for a given parent
+// placement and child tag-ID.
+func (im *IfdMapping) GetChild(parentPathPhrase string, tagId uint16) (mi *MappedIfd, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	mi, err = im.GetWithPath(parentPathPhrase)
+	log.PanicIf(err)
+
+	for _, childMi := range mi.Children {
+		if childMi.TagId == tagId {
+			return childMi, nil
+		}
+	}
+
+	// Whether or not an IFD is defined in data, such an IFD is not registered
+	// and would be unknown.
+	log.Panic(ErrChildIfdNotMapped)
+	return nil, nil
+}
+
+type IfdTagIdAndIndex struct {
+	Name  string
+	TagId uint16
+	Index int
+}
+
+func (itii IfdTagIdAndIndex) String() string {
+	return fmt.Sprintf("IfdTagIdAndIndex<NAME=[%s] ID=(%04x) INDEX=(%d)>", itii.Name, itii.TagId, itii.Index)
+}
+
+// ResolvePath takes a list of names, which can also be suffixed with indices
+// (to identify the second, third, etc.. sibling IFD) and returns a list of
+// tag-IDs and those indices.
+//
+// Example:
+//
+// - IFD/Exif/Iop
+// - IFD0/Exif/Iop
+//
+// This is the only call that supports adding the numeric indices.
+func (im *IfdMapping) ResolvePath(pathPhrase string) (lineage []IfdTagIdAndIndex, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	pathPhrase = strings.TrimSpace(pathPhrase)
+
+	if pathPhrase == "" {
+		log.Panicf("can not resolve empty path-phrase")
+	}
+
+	path := strings.Split(pathPhrase, "/")
+	lineage = make([]IfdTagIdAndIndex, len(path))
+
+	ptr := im.rootNode
+	empty := IfdTagIdAndIndex{}
+	for i, name := range path {
+		indexByte := name[len(name)-1]
+		index := 0
+		if indexByte >= '0' && indexByte <= '9' {
+			index = int(indexByte - '0')
+			name = name[:len(name)-1]
+		}
+
+		itii := IfdTagIdAndIndex{}
+		for _, mi := range ptr.Children {
+			if mi.Name != name {
+				continue
+			}
+
+			itii.Name = name
+			itii.TagId = mi.TagId
+			itii.Index = index
+
+			ptr = mi
+
+			break
+		}
+
+		if itii == empty {
+			log.Panicf("ifd child with name [%s] not registered: [%s]", name, pathPhrase)
+		}
+
+		lineage[i] = itii
+	}
+
+	return lineage, nil
+}
+
+func (im *IfdMapping) FqPathPhraseFromLineage(lineage []IfdTagIdAndIndex) (fqPathPhrase string) {
+	fqPathParts := make([]string, len(lineage))
+	for i, itii := range lineage {
+		if itii.Index > 0 {
+			fqPathParts[i] = fmt.Sprintf("%s%d", itii.Name, itii.Index)
+		} else {
+			fqPathParts[i] = itii.Name
+		}
+	}
+
+	return strings.Join(fqPathParts, "/")
+}
+
+func (im *IfdMapping) PathPhraseFromLineage(lineage []IfdTagIdAndIndex) (pathPhrase string) {
+	pathParts := make([]string, len(lineage))
+	for i, itii := range lineage {
+		pathParts[i] = itii.Name
+	}
+
+	return strings.Join(pathParts, "/")
+}
+
+// StripPathPhraseIndices returns a non-fully-qualified path-phrase (no
+// indices).
+func (im *IfdMapping) StripPathPhraseIndices(pathPhrase string) (strippedPathPhrase string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	lineage, err := im.ResolvePath(pathPhrase)
+	log.PanicIf(err)
+
+	strippedPathPhrase = im.PathPhraseFromLineage(lineage)
+	return strippedPathPhrase, nil
+}
+
+// Add puts the given IFD at the given position of the tree. The position of the
+// tree is referred to as the placement and is represented by a set of tag-IDs,
+// where the leftmost is the root tag and the tags going to the right are
+// progressive descendants.
+func (im *IfdMapping) Add(parentPlacement []uint16, tagId uint16, name string) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// TODO(dustin): !! It would be nicer to provide a list of names in the placement rather than tag-IDs.
+
+	ptr, err := im.Get(parentPlacement)
+	log.PanicIf(err)
+
+	path := make([]string, len(parentPlacement)+1)
+	if len(parentPlacement) > 0 {
+		copy(path, ptr.Path)
+	}
+
+	path[len(path)-1] = name
+
+	placement := make([]uint16, len(parentPlacement)+1)
+	if len(placement) > 0 {
+		copy(placement, ptr.Placement)
+	}
+
+	placement[len(placement)-1] = tagId
+
+	childIfd := &MappedIfd{
+		ParentTagId: ptr.TagId,
+		Path:        path,
+		Placement:   placement,
+		Name:        name,
+		TagId:       tagId,
+		Children:    make(map[uint16]*MappedIfd),
+	}
+
+	if _, found := ptr.Children[tagId]; found == true {
+		log.Panicf("child IFD with tag-ID (%04x) already registered under IFD [%s] with tag-ID (%04x)", tagId, ptr.Name, ptr.TagId)
+	}
+
+	ptr.Children[tagId] = childIfd
+
+	return nil
+}
+
+func (im *IfdMapping) dumpLineages(stack []*MappedIfd, input []string) (output []string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	currentIfd := stack[len(stack)-1]
+
+	output = input
+	for _, childIfd := range currentIfd.Children {
+		stackCopy := make([]*MappedIfd, len(stack)+1)
+
+		copy(stackCopy, stack)
+		stackCopy[len(stack)] = childIfd
+
+		// Add to output, but don't include the obligatory root node.
+		parts := make([]string, len(stackCopy)-1)
+		for i, mi := range stackCopy[1:] {
+			parts[i] = mi.Name
+		}
+
+		output = append(output, strings.Join(parts, "/"))
+
+		output, err = im.dumpLineages(stackCopy, output)
+		log.PanicIf(err)
+	}
+
+	return output, nil
+}
+
+func (im *IfdMapping) DumpLineages() (output []string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	stack := []*MappedIfd{im.rootNode}
+	output = make([]string, 0)
+
+	output, err = im.dumpLineages(stack, output)
+	log.PanicIf(err)
+
+	return output, nil
+}
+
+func LoadStandardIfds(im *IfdMapping) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	err = im.Add([]uint16{}, IfdRootId, IfdStandard)
+	log.PanicIf(err)
+
+	err = im.Add([]uint16{IfdRootId}, IfdExifId, IfdExif)
+	log.PanicIf(err)
+
+	err = im.Add([]uint16{IfdRootId, IfdExifId}, IfdIopId, IfdIop)
+	log.PanicIf(err)
+
+	err = im.Add([]uint16{IfdRootId}, IfdGpsId, IfdGps)
+	log.PanicIf(err)
+
+	return nil
+}
diff --git a/v2/ifd_builder.go b/v2/ifd_builder.go
new file mode 100644
index 0000000..40ef4dc
--- /dev/null
+++ b/v2/ifd_builder.go
@@ -0,0 +1,1265 @@
+package exif
+
+// NOTES:
+//
+// The thumbnail offset and length tags shouldn't be set directly. Use the
+// (*IfdBuilder).SetThumbnail() method instead.
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"encoding/binary"
+
+	"github.com/dsoprea/go-logging"
+)
+
+var (
+	ifdBuilderLogger = log.NewLogger("exif.ifd_builder")
+)
+
+var (
+	ErrTagEntryNotFound = errors.New("tag entry not found")
+	ErrChildIbNotFound  = errors.New("child IB not found")
+)
+
+type IfdBuilderTagValue struct {
+	valueBytes []byte
+	ib         *IfdBuilder
+}
+
+func (ibtv IfdBuilderTagValue) String() string {
+	if ibtv.IsBytes() == true {
+		var valuePhrase string
+		if len(ibtv.valueBytes) <= 8 {
+			valuePhrase = fmt.Sprintf("%v", ibtv.valueBytes)
+		} else {
+			valuePhrase = fmt.Sprintf("%v...", ibtv.valueBytes[:8])
+		}
+
+		return fmt.Sprintf("IfdBuilderTagValue<BYTES=%v LEN=(%d)>", valuePhrase, len(ibtv.valueBytes))
+	} else if ibtv.IsIb() == true {
+		return fmt.Sprintf("IfdBuilderTagValue<IB=%s>", ibtv.ib)
+	} else {
+		log.Panicf("IBTV state undefined")
+		return ""
+	}
+}
+
+func NewIfdBuilderTagValueFromBytes(valueBytes []byte) *IfdBuilderTagValue {
+	return &IfdBuilderTagValue{
+		valueBytes: valueBytes,
+	}
+}
+
+func NewIfdBuilderTagValueFromIfdBuilder(ib *IfdBuilder) *IfdBuilderTagValue {
+	return &IfdBuilderTagValue{
+		ib: ib,
+	}
+}
+
+// IsBytes returns true if the bytes are populated. This is always the case
+// when we're loaded from a tag in an existing IFD.
+func (ibtv IfdBuilderTagValue) IsBytes() bool {
+	return ibtv.valueBytes != nil
+}
+
+func (ibtv IfdBuilderTagValue) Bytes() []byte {
+	if ibtv.IsBytes() == false {
+		log.Panicf("this tag is not a byte-slice value")
+	} else if ibtv.IsIb() == true {
+		log.Panicf("this tag is an IFD-builder value not a byte-slice")
+	}
+
+	return ibtv.valueBytes
+}
+
+func (ibtv IfdBuilderTagValue) IsIb() bool {
+	return ibtv.ib != nil
+}
+
+func (ibtv IfdBuilderTagValue) Ib() *IfdBuilder {
+	if ibtv.IsIb() == false {
+		log.Panicf("this tag is not an IFD-builder value")
+	} else if ibtv.IsBytes() == true {
+		log.Panicf("this tag is a byte-slice, not a IFD-builder")
+	}
+
+	return ibtv.ib
+}
+
+type BuilderTag struct {
+	// ifdPath is the path of the IFD that hosts this tag.
+	ifdPath string
+
+	tagId  uint16
+	typeId TagTypePrimitive
+
+	// value is either a value that can be encoded, an IfdBuilder instance (for
+	// child IFDs), or an IfdTagEntry instance representing an existing,
+	// previously-stored tag.
+	value *IfdBuilderTagValue
+
+	// byteOrder is the byte order. It's chiefly/originally here to support
+	// printing the value.
+	byteOrder binary.ByteOrder
+}
+
+func NewBuilderTag(ifdPath string, tagId uint16, typeId TagTypePrimitive, value *IfdBuilderTagValue, byteOrder binary.ByteOrder) *BuilderTag {
+	return &BuilderTag{
+		ifdPath:   ifdPath,
+		tagId:     tagId,
+		typeId:    typeId,
+		value:     value,
+		byteOrder: byteOrder,
+	}
+}
+
+func NewChildIfdBuilderTag(ifdPath string, tagId uint16, value *IfdBuilderTagValue) *BuilderTag {
+	return &BuilderTag{
+		ifdPath: ifdPath,
+		tagId:   tagId,
+		typeId:  TypeLong,
+		value:   value,
+	}
+}
+
+func (bt *BuilderTag) Value() (value *IfdBuilderTagValue) {
+	return bt.value
+}
+
+func (bt *BuilderTag) String() string {
+	var valueString string
+
+	if bt.value.IsBytes() == true {
+		var err error
+
+		valueString, err = Format(bt.value.Bytes(), bt.typeId, false, bt.byteOrder)
+		log.PanicIf(err)
+	} else {
+		valueString = fmt.Sprintf("%v", bt.value)
+	}
+
+	return fmt.Sprintf("BuilderTag<IFD-PATH=[%s] TAG-ID=(0x%04x) TAG-TYPE=[%s] VALUE=[%s]>", bt.ifdPath, bt.tagId, TypeNames[bt.typeId], valueString)
+}
+
+func (bt *BuilderTag) SetValue(byteOrder binary.ByteOrder, value interface{}) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// TODO(dustin): !! Add test.
+
+	tt := NewTagType(bt.typeId, byteOrder)
+	ve := NewValueEncoder(byteOrder)
+
+	var ed EncodedData
+	if bt.typeId == TypeUndefined {
+		var err error
+
+		ed, err = EncodeUndefined(bt.ifdPath, bt.tagId, value)
+		log.PanicIf(err)
+	} else {
+		var err error
+
+		ed, err = ve.EncodeWithType(tt, value)
+		log.PanicIf(err)
+	}
+
+	bt.value = NewIfdBuilderTagValueFromBytes(ed.Encoded)
+
+	return nil
+}
+
+// NewStandardBuilderTag constructs a `BuilderTag` instance. The type is looked
+// up. `ii` is the type of IFD that owns this tag.
+func NewStandardBuilderTag(ifdPath string, it *IndexedTag, byteOrder binary.ByteOrder, value interface{}) *BuilderTag {
+	typeId := it.Type
+	tt := NewTagType(typeId, byteOrder)
+
+	ve := NewValueEncoder(byteOrder)
+
+	var ed EncodedData
+	if it.Type == TypeUndefined {
+		var err error
+
+		ed, err = EncodeUndefined(ifdPath, it.Id, value)
+		log.PanicIf(err)
+	} else {
+		var err error
+
+		ed, err = ve.EncodeWithType(tt, value)
+		log.PanicIf(err)
+	}
+
+	tagValue := NewIfdBuilderTagValueFromBytes(ed.Encoded)
+
+	return NewBuilderTag(
+		ifdPath,
+		it.Id,
+		typeId,
+		tagValue,
+		byteOrder)
+}
+
+type IfdBuilder struct {
+	// ifdName is the name of the IFD represented by this instance.
+	name string
+
+	// ifdPath is the path of the IFD represented by this instance.
+	ifdPath string
+
+	// fqIfdPath is the fully-qualified path of the IFD represented by this
+	// instance.
+	fqIfdPath string
+
+	// ifdTagId will be non-zero if we're a child IFD.
+	ifdTagId uint16
+
+	byteOrder binary.ByteOrder
+
+	// Includes both normal tags and IFD tags (which point to child IFDs).
+	// TODO(dustin): Keep a separate list of children like with `Ifd`.
+	// TODO(dustin): Either rename this or `Entries` in `Ifd` to be the same thing.
+	tags []*BuilderTag
+
+	// existingOffset will be the offset that this IFD is currently found at if
+	// it represents an IFD that has previously been stored (or 0 if not).
+	existingOffset uint32
+
+	// nextIb represents the next link if we're chaining to another.
+	nextIb *IfdBuilder
+
+	// thumbnailData is populated with thumbnail data if there was thumbnail
+	// data. Otherwise, it's nil.
+	thumbnailData []byte
+
+	ifdMapping *IfdMapping
+	tagIndex   *TagIndex
+}
+
+func NewIfdBuilder(ifdMapping *IfdMapping, tagIndex *TagIndex, fqIfdPath string, byteOrder binary.ByteOrder) (ib *IfdBuilder) {
+	ifdPath, err := ifdMapping.StripPathPhraseIndices(fqIfdPath)
+	log.PanicIf(err)
+
+	var ifdTagId uint16
+
+	mi, err := ifdMapping.GetWithPath(ifdPath)
+	if err == nil {
+		ifdTagId = mi.TagId
+	} else if log.Is(err, ErrChildIfdNotMapped) == false {
+		log.Panic(err)
+	}
+
+	ib = &IfdBuilder{
+		// The right-most part of the IFD-path.
+		name: mi.Name,
+
+		// ifdPath describes the current IFD placement within the IFD tree.
+		ifdPath: ifdPath,
+
+		// fqIfdPath describes the current IFD placement within the IFD tree as
+		// well as being qualified with non-zero indices.
+		fqIfdPath: fqIfdPath,
+
+		// ifdTagId is empty unless it's a child-IFD.
+		ifdTagId: ifdTagId,
+
+		byteOrder: byteOrder,
+		tags:      make([]*BuilderTag, 0),
+
+		ifdMapping: ifdMapping,
+		tagIndex:   tagIndex,
+	}
+
+	return ib
+}
+
+// NewIfdBuilderWithExistingIfd creates a new IB using the same header type
+// information as the given IFD.
+func NewIfdBuilderWithExistingIfd(ifd *Ifd) (ib *IfdBuilder) {
+	name := ifd.Name
+	ifdPath := ifd.IfdPath
+	fqIfdPath := ifd.FqIfdPath
+
+	var ifdTagId uint16
+
+	// There is no tag-ID for the root IFD. It will never be a child IFD.
+	if ifdPath != IfdPathStandard {
+		mi, err := ifd.ifdMapping.GetWithPath(ifdPath)
+		log.PanicIf(err)
+
+		ifdTagId = mi.TagId
+	}
+
+	ib = &IfdBuilder{
+		name:           name,
+		ifdPath:        ifdPath,
+		fqIfdPath:      fqIfdPath,
+		ifdTagId:       ifdTagId,
+		byteOrder:      ifd.ByteOrder,
+		existingOffset: ifd.Offset,
+		ifdMapping:     ifd.ifdMapping,
+		tagIndex:       ifd.tagIndex,
+	}
+
+	return ib
+}
+
+// NewIfdBuilderFromExistingChain creates a chain of IB instances from an
+// IFD chain generated from real data.
+func NewIfdBuilderFromExistingChain(rootIfd *Ifd, itevr *IfdTagEntryValueResolver) (firstIb *IfdBuilder) {
+	// OBSOLETE(dustin): Support for `itevr` is now obsolete. This parameter will be removed in the future.
+
+	var lastIb *IfdBuilder
+	i := 0
+	for thisExistingIfd := rootIfd; thisExistingIfd != nil; thisExistingIfd = thisExistingIfd.NextIfd {
+		newIb := NewIfdBuilder(rootIfd.ifdMapping, rootIfd.tagIndex, rootIfd.FqIfdPath, thisExistingIfd.ByteOrder)
+		if firstIb == nil {
+			firstIb = newIb
+		} else {
+			lastIb.SetNextIb(newIb)
+		}
+
+		err := newIb.AddTagsFromExisting(thisExistingIfd, nil, nil, nil)
+		log.PanicIf(err)
+
+		lastIb = newIb
+		i++
+	}
+
+	return firstIb
+}
+
+func (ib *IfdBuilder) NextIb() (nextIb *IfdBuilder, err error) {
+	return ib.nextIb, nil
+}
+
+func (ib *IfdBuilder) ChildWithTagId(childIfdTagId uint16) (childIb *IfdBuilder, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	for _, bt := range ib.tags {
+		if bt.value.IsIb() == false {
+			continue
+		}
+
+		childIbThis := bt.value.Ib()
+
+		if childIbThis.ifdTagId == childIfdTagId {
+			return childIbThis, nil
+		}
+	}
+
+	log.Panic(ErrChildIbNotFound)
+
+	// Never reached.
+	return nil, nil
+}
+
+func getOrCreateIbFromRootIbInner(rootIb *IfdBuilder, parentIb *IfdBuilder, currentLineage []IfdTagIdAndIndex) (ib *IfdBuilder, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// TODO(dustin): !! Add test.
+
+	thisIb := rootIb
+
+	// Since we're calling ourselves recursively with incrementally different
+	// paths, the FQ IFD-path of the parent that called us needs to be passed
+	// in, in order for us to know it.
+	var parentLineage []IfdTagIdAndIndex
+	if parentIb != nil {
+		var err error
+
+		parentLineage, err = thisIb.ifdMapping.ResolvePath(parentIb.fqIfdPath)
+		log.PanicIf(err)
+	}
+
+	// Process the current path part.
+	currentItii := currentLineage[0]
+
+	// Make sure the leftmost part of the FQ IFD-path agrees with the IB we
+	// were given.
+
+	expectedFqRootIfdPath := ""
+	if parentLineage != nil {
+		expectedLineage := append(parentLineage, currentItii)
+		expectedFqRootIfdPath = thisIb.ifdMapping.PathPhraseFromLineage(expectedLineage)
+	} else {
+		expectedFqRootIfdPath = thisIb.ifdMapping.PathPhraseFromLineage(currentLineage[:1])
+	}
+
+	if expectedFqRootIfdPath != thisIb.fqIfdPath {
+		log.Panicf("the FQ IFD-path [%s] we were given does not match the builder's FQ IFD-path [%s]", expectedFqRootIfdPath, thisIb.fqIfdPath)
+	}
+
+	// If we actually wanted a sibling (currentItii.Index > 0) then seek to it,
+	// appending new siblings, as required, until we get there.
+	for i := 0; i < currentItii.Index; i++ {
+		if thisIb.nextIb == nil {
+			// Generate an FQ IFD-path for the sibling. It'll use the same
+			// non-FQ IFD-path as the current IB.
+
+			siblingFqIfdPath := ""
+			if parentLineage != nil {
+				siblingFqIfdPath = fmt.Sprintf("%s/%s%d", parentIb.fqIfdPath, currentItii.Name, i+1)
+			} else {
+				siblingFqIfdPath = fmt.Sprintf("%s%d", currentItii.Name, i+1)
+			}
+
+			thisIb.nextIb = NewIfdBuilder(thisIb.ifdMapping, thisIb.tagIndex, siblingFqIfdPath, thisIb.byteOrder)
+		}
+
+		thisIb = thisIb.nextIb
+	}
+
+	// There is no child IFD to process. We're done.
+	if len(currentLineage) == 1 {
+		return thisIb, nil
+	}
+
+	// Establish the next child to be processed.
+
+	childItii := currentLineage[1]
+
+	var foundChild *IfdBuilder
+	for _, bt := range thisIb.tags {
+		if bt.value.IsIb() == false {
+			continue
+		}
+
+		childIb := bt.value.Ib()
+
+		if childIb.ifdTagId == childItii.TagId {
+			foundChild = childIb
+			break
+		}
+	}
+
+	// If we didn't find the child, add it.
+	if foundChild == nil {
+		thisIbLineage, err := thisIb.ifdMapping.ResolvePath(thisIb.fqIfdPath)
+		log.PanicIf(err)
+
+		childLineage := make([]IfdTagIdAndIndex, len(thisIbLineage)+1)
+		copy(childLineage, thisIbLineage)
+
+		childLineage[len(childLineage)-1] = childItii
+
+		fqIfdChildPath := thisIb.ifdMapping.FqPathPhraseFromLineage(childLineage)
+
+		foundChild = NewIfdBuilder(thisIb.ifdMapping, thisIb.tagIndex, fqIfdChildPath, thisIb.byteOrder)
+
+		err = thisIb.AddChildIb(foundChild)
+		log.PanicIf(err)
+	}
+
+	finalIb, err := getOrCreateIbFromRootIbInner(foundChild, thisIb, currentLineage[1:])
+	log.PanicIf(err)
+
+	return finalIb, nil
+}
+
+// GetOrCreateIbFromRootIb returns an IB representing the requested IFD, even if
+// an IB doesn't already exist for it. This function may call itself
+// recursively.
+func GetOrCreateIbFromRootIb(rootIb *IfdBuilder, fqIfdPath string) (ib *IfdBuilder, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// lineage is a necessity of our recursion process. It doesn't include any
+	// parent IFDs on its left-side; it starts with the current IB only.
+	lineage, err := rootIb.ifdMapping.ResolvePath(fqIfdPath)
+	log.PanicIf(err)
+
+	ib, err = getOrCreateIbFromRootIbInner(rootIb, nil, lineage)
+	log.PanicIf(err)
+
+	return ib, nil
+}
+
+func (ib *IfdBuilder) String() string {
+	nextIfdPhrase := ""
+	if ib.nextIb != nil {
+		// TODO(dustin): We were setting this to ii.String(), but we were getting hex-data when printing this after building from an existing chain.
+		nextIfdPhrase = ib.nextIb.ifdPath
+	}
+
+	return fmt.Sprintf("IfdBuilder<PATH=[%s] TAG-ID=(0x%04x) COUNT=(%d) OFF=(0x%04x) NEXT-IFD-PATH=[%s]>", ib.ifdPath, ib.ifdTagId, len(ib.tags), ib.existingOffset, nextIfdPhrase)
+}
+
+func (ib *IfdBuilder) Tags() (tags []*BuilderTag) {
+	return ib.tags
+}
+
+// SetThumbnail sets thumbnail data.
+//
+// NOTES:
+//
+// - We don't manage any facet of the thumbnail data. This is the
+//   responsibility of the user/developer.
+// - This method will fail unless the thumbnail is set on a the root IFD.
+//   However, in order to be valid, it must be set on the second one, linked to
+//   by the first, as per the EXIF/TIFF specification.
+// - We set the offset to (0) now but will allocate the data and properly assign
+//   the offset when the IB is encoded (later).
+func (ib *IfdBuilder) SetThumbnail(data []byte) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	if ib.ifdPath != IfdPathStandard {
+		log.Panicf("thumbnails can only go into a root Ifd (and only the second one)")
+	}
+
+	// TODO(dustin): !! Add a test for this function.
+
+	if data == nil || len(data) == 0 {
+		log.Panic("thumbnail is empty")
+	}
+
+	ib.thumbnailData = data
+
+	ibtvfb := NewIfdBuilderTagValueFromBytes(ib.thumbnailData)
+	offsetBt :=
+		NewBuilderTag(
+			ib.ifdPath,
+			ThumbnailOffsetTagId,
+			TypeLong,
+			ibtvfb,
+			ib.byteOrder)
+
+	err = ib.Set(offsetBt)
+	log.PanicIf(err)
+
+	thumbnailSizeIt, err := ib.tagIndex.Get(ib.ifdPath, ThumbnailSizeTagId)
+	log.PanicIf(err)
+
+	sizeBt := NewStandardBuilderTag(ib.ifdPath, thumbnailSizeIt, ib.byteOrder, []uint32{uint32(len(ib.thumbnailData))})
+
+	err = ib.Set(sizeBt)
+	log.PanicIf(err)
+
+	return nil
+}
+
+func (ib *IfdBuilder) Thumbnail() []byte {
+	return ib.thumbnailData
+}
+
+func (ib *IfdBuilder) printTagTree(levels int) {
+	indent := strings.Repeat(" ", levels*2)
+
+	i := 0
+	for currentIb := ib; currentIb != nil; currentIb = currentIb.nextIb {
+		prefix := " "
+		if i > 0 {
+			prefix = ">"
+		}
+
+		if levels == 0 {
+			fmt.Printf("%s%sIFD: %s INDEX=(%d)\n", indent, prefix, currentIb, i)
+		} else {
+			fmt.Printf("%s%sChild IFD: %s\n", indent, prefix, currentIb)
+		}
+
+		if len(currentIb.tags) > 0 {
+			fmt.Printf("\n")
+
+			for i, tag := range currentIb.tags {
+				isChildIb := false
+				_, err := ib.ifdMapping.GetChild(currentIb.ifdPath, tag.tagId)
+				if err == nil {
+					isChildIb = true
+				} else if log.Is(err, ErrChildIfdNotMapped) == false {
+					log.Panic(err)
+				}
+
+				tagName := ""
+
+				// If a normal tag (not a child IFD) get the name.
+				if isChildIb == true {
+					tagName = "<Child IFD>"
+				} else {
+					it, err := ib.tagIndex.Get(tag.ifdPath, tag.tagId)
+					if log.Is(err, ErrTagNotFound) == true {
+						tagName = "<UNKNOWN>"
+					} else if err != nil {
+						log.Panic(err)
+					} else {
+						tagName = it.Name
+					}
+				}
+
+				value := tag.Value()
+
+				if value.IsIb() == true {
+					fmt.Printf("%s  (%d): [%s] %s\n", indent, i, tagName, value.Ib())
+				} else {
+					fmt.Printf("%s  (%d): [%s] %s\n", indent, i, tagName, tag)
+				}
+
+				if isChildIb == true {
+					if tag.value.IsIb() == false {
+						log.Panicf("tag-ID (0x%04x) is an IFD but the tag value is not an IB instance: %v", tag.tagId, tag)
+					}
+
+					fmt.Printf("\n")
+
+					childIb := tag.value.Ib()
+					childIb.printTagTree(levels + 1)
+				}
+			}
+
+			fmt.Printf("\n")
+		}
+
+		i++
+	}
+}
+
+func (ib *IfdBuilder) PrintTagTree() {
+	ib.printTagTree(0)
+}
+
+func (ib *IfdBuilder) printIfdTree(levels int) {
+	indent := strings.Repeat(" ", levels*2)
+
+	i := 0
+	for currentIb := ib; currentIb != nil; currentIb = currentIb.nextIb {
+		prefix := " "
+		if i > 0 {
+			prefix = ">"
+		}
+
+		fmt.Printf("%s%s%s\n", indent, prefix, currentIb)
+
+		if len(currentIb.tags) > 0 {
+			for _, tag := range currentIb.tags {
+				isChildIb := false
+				_, err := ib.ifdMapping.GetChild(currentIb.ifdPath, tag.tagId)
+				if err == nil {
+					isChildIb = true
+				} else if log.Is(err, ErrChildIfdNotMapped) == false {
+					log.Panic(err)
+				}
+
+				if isChildIb == true {
+					if tag.value.IsIb() == false {
+						log.Panicf("tag-ID (0x%04x) is an IFD but the tag value is not an IB instance: %v", tag.tagId, tag)
+					}
+
+					childIb := tag.value.Ib()
+					childIb.printIfdTree(levels + 1)
+				}
+			}
+		}
+
+		i++
+	}
+}
+
+func (ib *IfdBuilder) PrintIfdTree() {
+	ib.printIfdTree(0)
+}
+
+func (ib *IfdBuilder) dumpToStrings(thisIb *IfdBuilder, prefix string, tagId uint16, lines []string) (linesOutput []string) {
+	if lines == nil {
+		linesOutput = make([]string, 0)
+	} else {
+		linesOutput = lines
+	}
+
+	siblingIfdIndex := 0
+	for ; thisIb != nil; thisIb = thisIb.nextIb {
+		line := fmt.Sprintf("IFD<PARENTS=[%s] FQ-IFD-PATH=[%s] IFD-INDEX=(%d) IFD-TAG-ID=(0x%04x) TAG=[0x%04x]>", prefix, thisIb.fqIfdPath, siblingIfdIndex, thisIb.ifdTagId, tagId)
+		linesOutput = append(linesOutput, line)
+
+		for i, tag := range thisIb.tags {
+			var childIb *IfdBuilder
+			childIfdName := ""
+			if tag.value.IsIb() == true {
+				childIb = tag.value.Ib()
+				childIfdName = childIb.ifdPath
+			}
+
+			line := fmt.Sprintf("TAG<PARENTS=[%s] FQ-IFD-PATH=[%s] IFD-TAG-ID=(0x%04x) CHILD-IFD=[%s] TAG-INDEX=(%d) TAG=[0x%04x]>", prefix, thisIb.fqIfdPath, thisIb.ifdTagId, childIfdName, i, tag.tagId)
+			linesOutput = append(linesOutput, line)
+
+			if childIb == nil {
+				continue
+			}
+
+			childPrefix := ""
+			if prefix == "" {
+				childPrefix = fmt.Sprintf("%s", thisIb.ifdPath)
+			} else {
+				childPrefix = fmt.Sprintf("%s->%s", prefix, thisIb.ifdPath)
+			}
+
+			linesOutput = thisIb.dumpToStrings(childIb, childPrefix, tag.tagId, linesOutput)
+		}
+
+		siblingIfdIndex++
+	}
+
+	return linesOutput
+}
+
+func (ib *IfdBuilder) DumpToStrings() (lines []string) {
+	return ib.dumpToStrings(ib, "", 0, lines)
+}
+
+func (ib *IfdBuilder) SetNextIb(nextIb *IfdBuilder) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	ib.nextIb = nextIb
+
+	return nil
+}
+
+func (ib *IfdBuilder) DeleteN(tagId uint16, n int) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	if n < 1 {
+		log.Panicf("N must be at least 1: (%d)", n)
+	}
+
+	for n > 0 {
+		j := -1
+		for i, bt := range ib.tags {
+			if bt.tagId == tagId {
+				j = i
+				break
+			}
+		}
+
+		if j == -1 {
+			log.Panic(ErrTagEntryNotFound)
+		}
+
+		ib.tags = append(ib.tags[:j], ib.tags[j+1:]...)
+		n--
+	}
+
+	return nil
+}
+
+func (ib *IfdBuilder) DeleteFirst(tagId uint16) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	err = ib.DeleteN(tagId, 1)
+	log.PanicIf(err)
+
+	return nil
+}
+
+func (ib *IfdBuilder) DeleteAll(tagId uint16) (n int, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	for {
+		err = ib.DeleteN(tagId, 1)
+		if log.Is(err, ErrTagEntryNotFound) == true {
+			break
+		} else if err != nil {
+			log.Panic(err)
+		}
+
+		n++
+	}
+
+	return n, nil
+}
+
+func (ib *IfdBuilder) ReplaceAt(position int, bt *BuilderTag) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	if position < 0 {
+		log.Panicf("replacement position must be 0 or greater")
+	} else if position >= len(ib.tags) {
+		log.Panicf("replacement position does not exist")
+	}
+
+	ib.tags[position] = bt
+
+	return nil
+}
+
+func (ib *IfdBuilder) Replace(tagId uint16, bt *BuilderTag) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	position, err := ib.Find(tagId)
+	log.PanicIf(err)
+
+	ib.tags[position] = bt
+
+	return nil
+}
+
+// Set will add a new entry or update an existing entry.
+func (ib *IfdBuilder) Set(bt *BuilderTag) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	position, err := ib.Find(bt.tagId)
+	if err == nil {
+		ib.tags[position] = bt
+	} else if log.Is(err, ErrTagEntryNotFound) == true {
+		err = ib.add(bt)
+		log.PanicIf(err)
+	} else {
+		log.Panic(err)
+	}
+
+	return nil
+}
+
+func (ib *IfdBuilder) FindN(tagId uint16, maxFound int) (found []int, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	found = make([]int, 0)
+
+	for i, bt := range ib.tags {
+		if bt.tagId == tagId {
+			found = append(found, i)
+			if maxFound == 0 || len(found) >= maxFound {
+				break
+			}
+		}
+	}
+
+	return found, nil
+}
+
+func (ib *IfdBuilder) Find(tagId uint16) (position int, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	found, err := ib.FindN(tagId, 1)
+	log.PanicIf(err)
+
+	if len(found) == 0 {
+		log.Panic(ErrTagEntryNotFound)
+	}
+
+	return found[0], nil
+}
+
+func (ib *IfdBuilder) FindTag(tagId uint16) (bt *BuilderTag, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	found, err := ib.FindN(tagId, 1)
+	log.PanicIf(err)
+
+	if len(found) == 0 {
+		log.Panic(ErrTagEntryNotFound)
+	}
+
+	position := found[0]
+
+	return ib.tags[position], nil
+}
+
+func (ib *IfdBuilder) FindTagWithName(tagName string) (bt *BuilderTag, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	it, err := ib.tagIndex.GetWithName(ib.ifdPath, tagName)
+	log.PanicIf(err)
+
+	found, err := ib.FindN(it.Id, 1)
+	log.PanicIf(err)
+
+	if len(found) == 0 {
+		log.Panic(ErrTagEntryNotFound)
+	}
+
+	position := found[0]
+
+	return ib.tags[position], nil
+}
+
+func (ib *IfdBuilder) add(bt *BuilderTag) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	if bt.ifdPath == "" {
+		log.Panicf("BuilderTag ifdPath is not set: %s", bt)
+	} else if bt.typeId == 0x0 {
+		log.Panicf("BuilderTag type-ID is not set: %s", bt)
+	} else if bt.value == nil {
+		log.Panicf("BuilderTag value is not set: %s", bt)
+	}
+
+	ib.tags = append(ib.tags, bt)
+	return nil
+}
+
+func (ib *IfdBuilder) Add(bt *BuilderTag) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	if bt.value.IsIb() == true {
+		log.Panicf("child IfdBuilders must be added via AddChildIb() or AddTagsFromExisting(), not Add()")
+	}
+
+	err = ib.add(bt)
+	log.PanicIf(err)
+
+	return nil
+}
+
+// AddChildIb adds a tag that branches to a new IFD.
+func (ib *IfdBuilder) AddChildIb(childIb *IfdBuilder) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	if childIb.ifdTagId == 0 {
+		log.Panicf("IFD can not be used as a child IFD (not associated with a tag-ID): %v", childIb)
+	} else if childIb.byteOrder != ib.byteOrder {
+		log.Panicf("Child IFD does not have the same byte-order: [%s] != [%s]", childIb.byteOrder, ib.byteOrder)
+	}
+
+	// Since no standard IFDs supports occuring more than once, check that a
+	// tag of this type has not been previously added. Note that we just search
+	// the current IFD and *not every* IFD.
+	for _, bt := range childIb.tags {
+		if bt.tagId == childIb.ifdTagId {
+			log.Panicf("child-IFD already added: %v", childIb.ifdPath)
+		}
+	}
+
+	bt := ib.NewBuilderTagFromBuilder(childIb)
+	ib.tags = append(ib.tags, bt)
+
+	return nil
+}
+
+func (ib *IfdBuilder) NewBuilderTagFromBuilder(childIb *IfdBuilder) (bt *BuilderTag) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.Panic(err)
+		}
+	}()
+
+	value := NewIfdBuilderTagValueFromIfdBuilder(childIb)
+
+	bt = NewChildIfdBuilderTag(
+		ib.ifdPath,
+		childIb.ifdTagId,
+		value)
+
+	return bt
+}
+
+// AddTagsFromExisting does a verbatim copy of the entries in `ifd` to this
+// builder. It excludes child IFDs. These must be added explicitly via
+// `AddChildIb()`.
+func (ib *IfdBuilder) AddTagsFromExisting(ifd *Ifd, itevr *IfdTagEntryValueResolver, includeTagIds []uint16, excludeTagIds []uint16) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// OBSOLETE(dustin): Support for `itevr` is now obsolete. This parameter will be removed in the future.
+
+	thumbnailData, err := ifd.Thumbnail()
+	if err == nil {
+		err = ib.SetThumbnail(thumbnailData)
+		log.PanicIf(err)
+	} else if log.Is(err, ErrNoThumbnail) == false {
+		log.Panic(err)
+	}
+
+	for i, ite := range ifd.Entries {
+		if ite.TagId == ThumbnailOffsetTagId || ite.TagId == ThumbnailSizeTagId {
+			// These will be added on-the-fly when we encode.
+			continue
+		}
+
+		if excludeTagIds != nil && len(excludeTagIds) > 0 {
+			found := false
+			for _, excludedTagId := range excludeTagIds {
+				if excludedTagId == ite.TagId {
+					found = true
+				}
+			}
+
+			if found == true {
+				continue
+			}
+		}
+
+		if includeTagIds != nil && len(includeTagIds) > 0 {
+			// Whether or not there was a list of excludes, if there is a list
+			// of includes than the current tag has to be in it.
+
+			found := false
+			for _, includedTagId := range includeTagIds {
+				if includedTagId == ite.TagId {
+					found = true
+					break
+				}
+			}
+
+			if found == false {
+				continue
+			}
+		}
+
+		var bt *BuilderTag
+
+		if ite.ChildIfdPath != "" {
+			// If we want to add an IFD tag, we'll have to build it first and
+			// *then* add it via a different method.
+
+			// Figure out which of the child-IFDs that are associated with
+			// this IFD represents this specific child IFD.
+
+			var childIfd *Ifd
+			for _, thisChildIfd := range ifd.Children {
+				if thisChildIfd.ParentTagIndex != i {
+					continue
+				} else if thisChildIfd.TagId != 0xffff && thisChildIfd.TagId != ite.TagId {
+					log.Panicf("child-IFD tag is not correct: TAG-POSITION=(%d) ITE=%s CHILD-IFD=%s", thisChildIfd.ParentTagIndex, ite, thisChildIfd)
+				}
+
+				childIfd = thisChildIfd
+				break
+			}
+
+			if childIfd == nil {
+				childTagIds := make([]string, len(ifd.Children))
+				for j, childIfd := range ifd.Children {
+					childTagIds[j] = fmt.Sprintf("0x%04x (parent tag-position %d)", childIfd.TagId, childIfd.ParentTagIndex)
+				}
+
+				log.Panicf("could not find child IFD for child ITE: IFD-PATH=[%s] TAG-ID=(0x%04x) CURRENT-TAG-POSITION=(%d) CHILDREN=%v", ite.IfdPath, ite.TagId, i, childTagIds)
+			}
+
+			childIb := NewIfdBuilderFromExistingChain(childIfd, nil)
+			bt = ib.NewBuilderTagFromBuilder(childIb)
+		} else {
+			// Non-IFD tag.
+
+			valueContext := ifd.GetValueContext(ite)
+
+			var rawBytes []byte
+
+			if ite.TagType == TypeUndefined {
+				// It's an undefined-type value. Try to process, or skip if
+				// we don't know how to.
+
+				undefinedInterface, err := valueContext.Undefined()
+				if err != nil {
+					if err == ErrUnhandledUnknownTypedTag {
+						// It's an undefined-type tag that we don't handle. If
+						// we don't know how to handle it, we can't know how
+						// many bytes it is and we must skip it.
+						continue
+					}
+
+					log.Panic(err)
+				}
+
+				undefined, ok := undefinedInterface.(UnknownTagValue)
+				if ok != true {
+					log.Panicf("unexpected value returned from undefined-value processor")
+				}
+
+				rawBytes, err = undefined.ValueBytes()
+				log.PanicIf(err)
+			} else {
+				// It's a value with a standard type.
+
+				var err error
+
+				rawBytes, err = valueContext.readRawEncoded()
+				log.PanicIf(err)
+			}
+
+			value := NewIfdBuilderTagValueFromBytes(rawBytes)
+
+			bt = NewBuilderTag(
+				ifd.IfdPath,
+				ite.TagId,
+				ite.TagType,
+				value,
+				ib.byteOrder)
+		}
+
+		err := ib.add(bt)
+		log.PanicIf(err)
+	}
+
+	return nil
+}
+
+// AddStandard quickly and easily composes and adds the tag using the
+// information already known about a tag. Only works with standard tags.
+func (ib *IfdBuilder) AddStandard(tagId uint16, value interface{}) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	it, err := ib.tagIndex.Get(ib.ifdPath, tagId)
+	log.PanicIf(err)
+
+	bt := NewStandardBuilderTag(ib.ifdPath, it, ib.byteOrder, value)
+
+	err = ib.add(bt)
+	log.PanicIf(err)
+
+	return nil
+}
+
+// AddStandardWithName quickly and easily composes and adds the tag using the
+// information already known about a tag (using the name). Only works with
+// standard tags.
+func (ib *IfdBuilder) AddStandardWithName(tagName string, value interface{}) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	it, err := ib.tagIndex.GetWithName(ib.ifdPath, tagName)
+	log.PanicIf(err)
+
+	bt := NewStandardBuilderTag(ib.ifdPath, it, ib.byteOrder, value)
+
+	err = ib.add(bt)
+	log.PanicIf(err)
+
+	return nil
+}
+
+// SetStandard quickly and easily composes and adds or replaces the tag using
+// the information already known about a tag. Only works with standard tags.
+func (ib *IfdBuilder) SetStandard(tagId uint16, value interface{}) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// TODO(dustin): !! Add test for this function.
+
+	it, err := ib.tagIndex.Get(ib.ifdPath, tagId)
+	log.PanicIf(err)
+
+	bt := NewStandardBuilderTag(ib.ifdPath, it, ib.byteOrder, value)
+
+	i, err := ib.Find(tagId)
+	if err != nil {
+		if log.Is(err, ErrTagEntryNotFound) == false {
+			log.Panic(err)
+		}
+
+		ib.tags = append(ib.tags, bt)
+	} else {
+		ib.tags[i] = bt
+	}
+
+	return nil
+}
+
+// SetStandardWithName quickly and easily composes and adds or replaces the
+// tag using the information already known about a tag (using the name). Only
+// works with standard tags.
+func (ib *IfdBuilder) SetStandardWithName(tagName string, value interface{}) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// TODO(dustin): !! Add test for this function.
+
+	it, err := ib.tagIndex.GetWithName(ib.ifdPath, tagName)
+	log.PanicIf(err)
+
+	bt := NewStandardBuilderTag(ib.ifdPath, it, ib.byteOrder, value)
+
+	i, err := ib.Find(bt.tagId)
+	if err != nil {
+		if log.Is(err, ErrTagEntryNotFound) == false {
+			log.Panic(err)
+		}
+
+		ib.tags = append(ib.tags, bt)
+	} else {
+		ib.tags[i] = bt
+	}
+
+	return nil
+}
diff --git a/v2/ifd_builder_encode.go b/v2/ifd_builder_encode.go
new file mode 100644
index 0000000..90fb2dd
--- /dev/null
+++ b/v2/ifd_builder_encode.go
@@ -0,0 +1,530 @@
+package exif
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+
+	"encoding/binary"
+
+	"github.com/dsoprea/go-logging"
+)
+
+const (
+	// Tag-ID + Tag-Type + Unit-Count + Value/Offset.
+	IfdTagEntrySize = uint32(2 + 2 + 4 + 4)
+)
+
+type ByteWriter struct {
+	b         *bytes.Buffer
+	byteOrder binary.ByteOrder
+}
+
+func NewByteWriter(b *bytes.Buffer, byteOrder binary.ByteOrder) (bw *ByteWriter) {
+	return &ByteWriter{
+		b:         b,
+		byteOrder: byteOrder,
+	}
+}
+
+func (bw ByteWriter) writeAsBytes(value interface{}) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	err = binary.Write(bw.b, bw.byteOrder, value)
+	log.PanicIf(err)
+
+	return nil
+}
+
+func (bw ByteWriter) WriteUint32(value uint32) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	err = bw.writeAsBytes(value)
+	log.PanicIf(err)
+
+	return nil
+}
+
+func (bw ByteWriter) WriteUint16(value uint16) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	err = bw.writeAsBytes(value)
+	log.PanicIf(err)
+
+	return nil
+}
+
+func (bw ByteWriter) WriteFourBytes(value []byte) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	len_ := len(value)
+	if len_ != 4 {
+		log.Panicf("value is not four-bytes: (%d)", len_)
+	}
+
+	_, err = bw.b.Write(value)
+	log.PanicIf(err)
+
+	return nil
+}
+
+// ifdOffsetIterator keeps track of where the next IFD should be written by
+// keeping track of where the offsets start, the data that has been added, and
+// bumping the offset *when* the data is added.
+type ifdDataAllocator struct {
+	offset uint32
+	b      bytes.Buffer
+}
+
+func newIfdDataAllocator(ifdDataAddressableOffset uint32) *ifdDataAllocator {
+	return &ifdDataAllocator{
+		offset: ifdDataAddressableOffset,
+	}
+}
+
+func (ida *ifdDataAllocator) Allocate(value []byte) (offset uint32, err error) {
+	_, err = ida.b.Write(value)
+	log.PanicIf(err)
+
+	offset = ida.offset
+	ida.offset += uint32(len(value))
+
+	return offset, nil
+}
+
+func (ida *ifdDataAllocator) NextOffset() uint32 {
+	return ida.offset
+}
+
+func (ida *ifdDataAllocator) Bytes() []byte {
+	return ida.b.Bytes()
+}
+
+// IfdByteEncoder converts an IB to raw bytes (for writing) while also figuring
+// out all of the allocations and indirection that is required for extended
+// data.
+type IfdByteEncoder struct {
+	// journal holds a list of actions taken while encoding.
+	journal [][3]string
+}
+
+func NewIfdByteEncoder() (ibe *IfdByteEncoder) {
+	return &IfdByteEncoder{
+		journal: make([][3]string, 0),
+	}
+}
+
+func (ibe *IfdByteEncoder) Journal() [][3]string {
+	return ibe.journal
+}
+
+func (ibe *IfdByteEncoder) TableSize(entryCount int) uint32 {
+	// Tag-Count + (Entry-Size * Entry-Count) + Next-IFD-Offset.
+	return uint32(2) + (IfdTagEntrySize * uint32(entryCount)) + uint32(4)
+}
+
+func (ibe *IfdByteEncoder) pushToJournal(where, direction, format string, args ...interface{}) {
+	event := [3]string{
+		direction,
+		where,
+		fmt.Sprintf(format, args...),
+	}
+
+	ibe.journal = append(ibe.journal, event)
+}
+
+// PrintJournal prints a hierarchical representation of the steps taken during
+// encoding.
+func (ibe *IfdByteEncoder) PrintJournal() {
+	maxWhereLength := 0
+	for _, event := range ibe.journal {
+		where := event[1]
+
+		len_ := len(where)
+		if len_ > maxWhereLength {
+			maxWhereLength = len_
+		}
+	}
+
+	level := 0
+	for i, event := range ibe.journal {
+		direction := event[0]
+		where := event[1]
+		message := event[2]
+
+		if direction != ">" && direction != "<" && direction != "-" {
+			log.Panicf("journal operation not valid: [%s]", direction)
+		}
+
+		if direction == "<" {
+			if level <= 0 {
+				log.Panicf("journal operations unbalanced (too many closes)")
+			}
+
+			level--
+		}
+
+		indent := strings.Repeat("  ", level)
+
+		fmt.Printf("%3d %s%s %s: %s\n", i, indent, direction, where, message)
+
+		if direction == ">" {
+			level++
+		}
+	}
+
+	if level != 0 {
+		log.Panicf("journal operations unbalanced (too many opens)")
+	}
+}
+
+// encodeTagToBytes encodes the given tag to a byte stream. If
+// `nextIfdOffsetToWrite` is more than (0), recurse into child IFDs
+// (`nextIfdOffsetToWrite` is required in order for them to know where the its
+// IFD data will be written, in order for them to know the offset of where
+// their allocated-data block will start, which follows right behind).
+func (ibe *IfdByteEncoder) encodeTagToBytes(ib *IfdBuilder, bt *BuilderTag, bw *ByteWriter, ida *ifdDataAllocator, nextIfdOffsetToWrite uint32) (childIfdBlock []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// Write tag-ID.
+	err = bw.WriteUint16(bt.tagId)
+	log.PanicIf(err)
+
+	// Works for both values and child IFDs (which have an official size of
+	// LONG).
+	err = bw.WriteUint16(uint16(bt.typeId))
+	log.PanicIf(err)
+
+	// Write unit-count.
+
+	if bt.value.IsBytes() == true {
+		effectiveType := bt.typeId
+		if bt.typeId == TypeUndefined {
+			effectiveType = TypeByte
+		}
+
+		// It's a non-unknown value.Calculate the count of values of
+		// the type that we're writing and the raw bytes for the whole list.
+
+		typeSize := uint32(effectiveType.Size())
+
+		valueBytes := bt.value.Bytes()
+
+		len_ := len(valueBytes)
+		unitCount := uint32(len_) / typeSize
+
+		if _, found := tagsWithoutAlignment[bt.tagId]; found == false {
+			remainder := uint32(len_) % typeSize
+
+			if remainder > 0 {
+				log.Panicf("tag (0x%04x) value of (%d) bytes not evenly divisible by type-size (%d)", bt.tagId, len_, typeSize)
+			}
+		}
+
+		err = bw.WriteUint32(unitCount)
+		log.PanicIf(err)
+
+		// Write four-byte value/offset.
+
+		if len_ > 4 {
+			offset, err := ida.Allocate(valueBytes)
+			log.PanicIf(err)
+
+			err = bw.WriteUint32(offset)
+			log.PanicIf(err)
+		} else {
+			fourBytes := make([]byte, 4)
+			copy(fourBytes, valueBytes)
+
+			err = bw.WriteFourBytes(fourBytes)
+			log.PanicIf(err)
+		}
+	} else {
+		if bt.value.IsIb() == false {
+			log.Panicf("tag value is not a byte-slice but also not a child IB: %v", bt)
+		}
+
+		// Write unit-count (one LONG representing one offset).
+		err = bw.WriteUint32(1)
+		log.PanicIf(err)
+
+		if nextIfdOffsetToWrite > 0 {
+			var err error
+
+			ibe.pushToJournal("encodeTagToBytes", ">", "[%s]->[%s]", ib.ifdPath, bt.value.Ib().ifdPath)
+
+			// Create the block of IFD data and everything it requires.
+			childIfdBlock, err = ibe.encodeAndAttachIfd(bt.value.Ib(), nextIfdOffsetToWrite)
+			log.PanicIf(err)
+
+			ibe.pushToJournal("encodeTagToBytes", "<", "[%s]->[%s]", bt.value.Ib().ifdPath, ib.ifdPath)
+
+			// Use the next-IFD offset for it. The IFD will actually get
+			// attached after we return.
+			err = bw.WriteUint32(nextIfdOffsetToWrite)
+			log.PanicIf(err)
+
+		} else {
+			// No child-IFDs are to be allocated. Finish the entry with a NULL
+			// pointer.
+
+			ibe.pushToJournal("encodeTagToBytes", "-", "*Not* descending to child: [%s]", bt.value.Ib().ifdPath)
+
+			err = bw.WriteUint32(0)
+			log.PanicIf(err)
+		}
+	}
+
+	return childIfdBlock, nil
+}
+
+// encodeIfdToBytes encodes the given IB to a byte-slice. We are given the
+// offset at which this IFD will be written. This method is used called both to
+// pre-determine how big the table is going to be (so that we can calculate the
+// address to allocate data at) as well as to write the final table.
+//
+// It is necessary to fully realize the table in order to predetermine its size
+// because it is not enough to know the size of the table: If there are child
+// IFDs, we will not be able to allocate them without first knowing how much
+// data we need to allocate for the current IFD.
+func (ibe *IfdByteEncoder) encodeIfdToBytes(ib *IfdBuilder, ifdAddressableOffset uint32, nextIfdOffsetToWrite uint32, setNextIb bool) (data []byte, tableSize uint32, dataSize uint32, childIfdSizes []uint32, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	ibe.pushToJournal("encodeIfdToBytes", ">", "%s", ib)
+
+	tableSize = ibe.TableSize(len(ib.tags))
+
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, ib.byteOrder)
+
+	// Write tag count.
+	err = bw.WriteUint16(uint16(len(ib.tags)))
+	log.PanicIf(err)
+
+	ida := newIfdDataAllocator(ifdAddressableOffset)
+
+	childIfdBlocks := make([][]byte, 0)
+
+	// Write raw bytes for each tag entry. Allocate larger data to be referred
+	// to in the follow-up data-block as required. Any "unknown"-byte tags that
+	// we can't parse will not be present here (using AddTagsFromExisting(), at
+	// least).
+	for _, bt := range ib.tags {
+		childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, nextIfdOffsetToWrite)
+		log.PanicIf(err)
+
+		if childIfdBlock != nil {
+			// We aren't allowed to have non-nil child IFDs if we're just
+			// sizing things up.
+			if nextIfdOffsetToWrite == 0 {
+				log.Panicf("no IFD offset provided for child-IFDs; no new child-IFDs permitted")
+			}
+
+			nextIfdOffsetToWrite += uint32(len(childIfdBlock))
+			childIfdBlocks = append(childIfdBlocks, childIfdBlock)
+		}
+	}
+
+	dataBytes := ida.Bytes()
+	dataSize = uint32(len(dataBytes))
+
+	childIfdSizes = make([]uint32, len(childIfdBlocks))
+	childIfdsTotalSize := uint32(0)
+	for i, childIfdBlock := range childIfdBlocks {
+		len_ := uint32(len(childIfdBlock))
+		childIfdSizes[i] = len_
+		childIfdsTotalSize += len_
+	}
+
+	// N the link from this IFD to the next IFD that will be written in the
+	// next cycle.
+	if setNextIb == true {
+		// Write address of next IFD in chain. This will be the original
+		// allocation offset plus the size of everything we have allocated for
+		// this IFD and its child-IFDs.
+		//
+		// It is critical that this number is stepped properly. We experienced
+		// an issue whereby it first looked like we were duplicating the IFD and
+		// then that we were duplicating the tags in the wrong IFD, and then
+		// finally we determined that the next-IFD offset for the first IFD was
+		// accidentally pointing back to the EXIF IFD, so we were visiting it
+		// twice when visiting through the tags after decoding. It was an
+		// expensive bug to find.
+
+		ibe.pushToJournal("encodeIfdToBytes", "-", "Setting 'next' IFD to (0x%08x).", nextIfdOffsetToWrite)
+
+		err := bw.WriteUint32(nextIfdOffsetToWrite)
+		log.PanicIf(err)
+	} else {
+		err := bw.WriteUint32(0)
+		log.PanicIf(err)
+	}
+
+	_, err = b.Write(dataBytes)
+	log.PanicIf(err)
+
+	// Append any child IFD blocks after our table and data blocks. These IFDs
+	// were equipped with the appropriate offset information so it's expected
+	// that all offsets referred to by these will be correct.
+	//
+	// Note that child-IFDs are append after the current IFD and before the
+	// next IFD, as opposed to the root IFDs, which are chained together but
+	// will be interrupted by these child-IFDs (which is expected, per the
+	// standard).
+
+	for _, childIfdBlock := range childIfdBlocks {
+		_, err = b.Write(childIfdBlock)
+		log.PanicIf(err)
+	}
+
+	ibe.pushToJournal("encodeIfdToBytes", "<", "%s", ib)
+
+	return b.Bytes(), tableSize, dataSize, childIfdSizes, nil
+}
+
+// encodeAndAttachIfd is a reentrant function that processes the IFD chain.
+func (ibe *IfdByteEncoder) encodeAndAttachIfd(ib *IfdBuilder, ifdAddressableOffset uint32) (data []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	ibe.pushToJournal("encodeAndAttachIfd", ">", "%s", ib)
+
+	b := new(bytes.Buffer)
+
+	i := 0
+
+	for thisIb := ib; thisIb != nil; thisIb = thisIb.nextIb {
+
+		// Do a dry-run in order to pre-determine its size requirement.
+
+		ibe.pushToJournal("encodeAndAttachIfd", ">", "Beginning encoding process: (%d) [%s]", i, thisIb.ifdPath)
+
+		ibe.pushToJournal("encodeAndAttachIfd", ">", "Calculating size: (%d) [%s]", i, thisIb.ifdPath)
+
+		_, tableSize, allocatedDataSize, _, err := ibe.encodeIfdToBytes(thisIb, ifdAddressableOffset, 0, false)
+		log.PanicIf(err)
+
+		ibe.pushToJournal("encodeAndAttachIfd", "<", "Finished calculating size: (%d) [%s]", i, thisIb.ifdPath)
+
+		ifdAddressableOffset += tableSize
+		nextIfdOffsetToWrite := ifdAddressableOffset + allocatedDataSize
+
+		ibe.pushToJournal("encodeAndAttachIfd", ">", "Next IFD will be written at offset (0x%08x)", nextIfdOffsetToWrite)
+
+		// Write our IFD as well as any child-IFDs (now that we know the offset
+		// where new IFDs and their data will be allocated).
+
+		setNextIb := thisIb.nextIb != nil
+
+		ibe.pushToJournal("encodeAndAttachIfd", ">", "Encoding starting: (%d) [%s] NEXT-IFD-OFFSET-TO-WRITE=(0x%08x)", i, thisIb.ifdPath, nextIfdOffsetToWrite)
+
+		tableAndAllocated, effectiveTableSize, effectiveAllocatedDataSize, childIfdSizes, err :=
+			ibe.encodeIfdToBytes(thisIb, ifdAddressableOffset, nextIfdOffsetToWrite, setNextIb)
+
+		log.PanicIf(err)
+
+		if effectiveTableSize != tableSize {
+			log.Panicf("written table size does not match the pre-calculated table size: (%d) != (%d) %s", effectiveTableSize, tableSize, ib)
+		} else if effectiveAllocatedDataSize != allocatedDataSize {
+			log.Panicf("written allocated-data size does not match the pre-calculated allocated-data size: (%d) != (%d) %s", effectiveAllocatedDataSize, allocatedDataSize, ib)
+		}
+
+		ibe.pushToJournal("encodeAndAttachIfd", "<", "Encoding done: (%d) [%s]", i, thisIb.ifdPath)
+
+		totalChildIfdSize := uint32(0)
+		for _, childIfdSize := range childIfdSizes {
+			totalChildIfdSize += childIfdSize
+		}
+
+		if len(tableAndAllocated) != int(tableSize+allocatedDataSize+totalChildIfdSize) {
+			log.Panicf("IFD table and data is not a consistent size: (%d) != (%d)", len(tableAndAllocated), tableSize+allocatedDataSize+totalChildIfdSize)
+		}
+
+		// TODO(dustin): We might want to verify the original tableAndAllocated length, too.
+
+		_, err = b.Write(tableAndAllocated)
+		log.PanicIf(err)
+
+		// Advance past what we've allocated, thus far.
+
+		ifdAddressableOffset += allocatedDataSize + totalChildIfdSize
+
+		ibe.pushToJournal("encodeAndAttachIfd", "<", "Finishing encoding process: (%d) [%s] [FINAL:] NEXT-IFD-OFFSET-TO-WRITE=(0x%08x)", i, ib.ifdPath, nextIfdOffsetToWrite)
+
+		i++
+	}
+
+	ibe.pushToJournal("encodeAndAttachIfd", "<", "%s", ib)
+
+	return b.Bytes(), nil
+}
+
+// EncodeToExifPayload is the base encoding step that transcribes the entire IB
+// structure to its on-disk layout.
+func (ibe *IfdByteEncoder) EncodeToExifPayload(ib *IfdBuilder) (data []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	data, err = ibe.encodeAndAttachIfd(ib, ExifDefaultFirstIfdOffset)
+	log.PanicIf(err)
+
+	return data, nil
+}
+
+// EncodeToExif calls EncodeToExifPayload and then packages the result into a
+// complete EXIF block.
+func (ibe *IfdByteEncoder) EncodeToExif(ib *IfdBuilder) (data []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	encodedIfds, err := ibe.EncodeToExifPayload(ib)
+	log.PanicIf(err)
+
+	// Wrap the IFD in a formal EXIF block.
+
+	b := new(bytes.Buffer)
+
+	headerBytes, err := BuildExifHeader(ib.byteOrder, ExifDefaultFirstIfdOffset)
+	log.PanicIf(err)
+
+	_, err = b.Write(headerBytes)
+	log.PanicIf(err)
+
+	_, err = b.Write(encodedIfds)
+	log.PanicIf(err)
+
+	return b.Bytes(), nil
+}
diff --git a/v2/ifd_builder_encode_test.go b/v2/ifd_builder_encode_test.go
new file mode 100644
index 0000000..f2a8b12
--- /dev/null
+++ b/v2/ifd_builder_encode_test.go
@@ -0,0 +1,906 @@
+package exif
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/dsoprea/go-logging"
+)
+
+func Test_ByteWriter_writeAsBytes_uint8(t *testing.T) {
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, TestDefaultByteOrder)
+
+	err := bw.writeAsBytes(uint8(0x12))
+	log.PanicIf(err)
+
+	if bytes.Compare(b.Bytes(), []byte{0x12}) != 0 {
+		t.Fatalf("uint8 not encoded correctly.")
+	}
+}
+
+func Test_ByteWriter_writeAsBytes_uint16(t *testing.T) {
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, TestDefaultByteOrder)
+
+	err := bw.writeAsBytes(uint16(0x1234))
+	log.PanicIf(err)
+
+	if bytes.Compare(b.Bytes(), []byte{0x12, 0x34}) != 0 {
+		t.Fatalf("uint16 not encoded correctly.")
+	}
+}
+
+func Test_ByteWriter_writeAsBytes_uint32(t *testing.T) {
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, TestDefaultByteOrder)
+
+	err := bw.writeAsBytes(uint32(0x12345678))
+	log.PanicIf(err)
+
+	if bytes.Compare(b.Bytes(), []byte{0x12, 0x34, 0x56, 0x78}) != 0 {
+		t.Fatalf("uint32 not encoded correctly.")
+	}
+}
+
+func Test_ByteWriter_WriteUint16(t *testing.T) {
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, TestDefaultByteOrder)
+
+	err := bw.WriteUint16(uint16(0x1234))
+	log.PanicIf(err)
+
+	if bytes.Compare(b.Bytes(), []byte{0x12, 0x34}) != 0 {
+		t.Fatalf("uint16 not encoded correctly (as bytes).")
+	}
+}
+
+func Test_ByteWriter_WriteUint32(t *testing.T) {
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, TestDefaultByteOrder)
+
+	err := bw.WriteUint32(uint32(0x12345678))
+	log.PanicIf(err)
+
+	if bytes.Compare(b.Bytes(), []byte{0x12, 0x34, 0x56, 0x78}) != 0 {
+		t.Fatalf("uint32 not encoded correctly (as bytes).")
+	}
+}
+
+func Test_ByteWriter_WriteFourBytes(t *testing.T) {
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, TestDefaultByteOrder)
+
+	err := bw.WriteFourBytes([]byte{0x11, 0x22, 0x33, 0x44})
+	log.PanicIf(err)
+
+	if bytes.Compare(b.Bytes(), []byte{0x11, 0x22, 0x33, 0x44}) != 0 {
+		t.Fatalf("four-bytes not encoded correctly.")
+	}
+}
+
+func Test_ByteWriter_WriteFourBytes_TooMany(t *testing.T) {
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, TestDefaultByteOrder)
+
+	err := bw.WriteFourBytes([]byte{0x11, 0x22, 0x33, 0x44, 0x55})
+	if err == nil {
+		t.Fatalf("expected error for not exactly four-bytes")
+	} else if err.Error() != "value is not four-bytes: (5)" {
+		t.Fatalf("wrong error for not exactly four bytes: %v", err)
+	}
+}
+
+func Test_IfdDataAllocator_Allocate_InitialOffset1(t *testing.T) {
+	addressableOffset := uint32(0)
+	ida := newIfdDataAllocator(addressableOffset)
+
+	if ida.NextOffset() != addressableOffset {
+		t.Fatalf("initial offset not correct: (%d) != (%d)", ida.NextOffset(), addressableOffset)
+	} else if len(ida.Bytes()) != 0 {
+		t.Fatalf("initial buffer not empty")
+	}
+
+	data := []byte{0x1, 0x2, 0x3}
+	offset, err := ida.Allocate(data)
+	log.PanicIf(err)
+
+	expected := uint32(addressableOffset + 0)
+	if offset != expected {
+		t.Fatalf("offset not bumped correctly (2): (%d) != (%d)", offset, expected)
+	} else if ida.NextOffset() != offset+uint32(3) {
+		t.Fatalf("position counter not advanced properly")
+	} else if bytes.Compare(ida.Bytes(), []byte{0x1, 0x2, 0x3}) != 0 {
+		t.Fatalf("buffer not correct after write (1)")
+	}
+
+	data = []byte{0x4, 0x5, 0x6}
+	offset, err = ida.Allocate(data)
+	log.PanicIf(err)
+
+	expected = uint32(addressableOffset + 3)
+	if offset != expected {
+		t.Fatalf("offset not bumped correctly (3): (%d) != (%d)", offset, expected)
+	} else if ida.NextOffset() != offset+uint32(3) {
+		t.Fatalf("position counter not advanced properly")
+	} else if bytes.Compare(ida.Bytes(), []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}) != 0 {
+		t.Fatalf("buffer not correct after write (2)")
+	}
+}
+
+func Test_IfdDataAllocator_Allocate_InitialOffset2(t *testing.T) {
+	addressableOffset := uint32(10)
+	ida := newIfdDataAllocator(addressableOffset)
+
+	if ida.NextOffset() != addressableOffset {
+		t.Fatalf("initial offset not correct: (%d) != (%d)", ida.NextOffset(), addressableOffset)
+	} else if len(ida.Bytes()) != 0 {
+		t.Fatalf("initial buffer not empty")
+	}
+
+	data := []byte{0x1, 0x2, 0x3}
+	offset, err := ida.Allocate(data)
+	log.PanicIf(err)
+
+	expected := uint32(addressableOffset + 0)
+	if offset != expected {
+		t.Fatalf("offset not bumped correctly (2): (%d) != (%d)", offset, expected)
+	} else if ida.NextOffset() != offset+uint32(3) {
+		t.Fatalf("position counter not advanced properly")
+	} else if bytes.Compare(ida.Bytes(), []byte{0x1, 0x2, 0x3}) != 0 {
+		t.Fatalf("buffer not correct after write (1)")
+	}
+
+	data = []byte{0x4, 0x5, 0x6}
+	offset, err = ida.Allocate(data)
+	log.PanicIf(err)
+
+	expected = uint32(addressableOffset + 3)
+	if offset != expected {
+		t.Fatalf("offset not bumped correctly (3): (%d) != (%d)", offset, expected)
+	} else if ida.NextOffset() != offset+uint32(3) {
+		t.Fatalf("position counter not advanced properly")
+	} else if bytes.Compare(ida.Bytes(), []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}) != 0 {
+		t.Fatalf("buffer not correct after write (2)")
+	}
+}
+
+func Test_IfdByteEncoder__Arithmetic(t *testing.T) {
+	ibe := NewIfdByteEncoder()
+
+	if (ibe.TableSize(1) - ibe.TableSize(0)) != IfdTagEntrySize {
+		t.Fatalf("table-size/entry-size not consistent (1)")
+	} else if (ibe.TableSize(11) - ibe.TableSize(10)) != IfdTagEntrySize {
+		t.Fatalf("table-size/entry-size not consistent (2)")
+	}
+}
+
+func Test_IfdByteEncoder_encodeTagToBytes_bytes_embedded1(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Test failure.")
+			panic(err)
+		}
+	}()
+
+	ibe := NewIfdByteEncoder()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandardGps, TestDefaultByteOrder)
+
+	it, err := ti.Get(ib.ifdPath, uint16(0x0000))
+	log.PanicIf(err)
+
+	bt := NewStandardBuilderTag(IfdPathStandardGps, it, TestDefaultByteOrder, []uint8{uint8(0x12)})
+
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, TestDefaultByteOrder)
+
+	addressableOffset := uint32(0x1234)
+	ida := newIfdDataAllocator(addressableOffset)
+
+	childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, uint32(0))
+	log.PanicIf(err)
+
+	if childIfdBlock != nil {
+		t.Fatalf("no child-IFDs were expected to be allocated")
+	} else if bytes.Compare(b.Bytes(), []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x12, 0x00, 0x00, 0x00}) != 0 {
+		t.Fatalf("encoded tag-entry bytes not correct")
+	} else if ida.NextOffset() != addressableOffset {
+		t.Fatalf("allocation was done but not expected")
+	}
+}
+
+func Test_IfdByteEncoder_encodeTagToBytes_bytes_embedded2(t *testing.T) {
+	ibe := NewIfdByteEncoder()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandardGps, TestDefaultByteOrder)
+
+	it, err := ti.Get(ib.ifdPath, uint16(0x0000))
+	log.PanicIf(err)
+
+	bt := NewStandardBuilderTag(IfdPathStandardGps, it, TestDefaultByteOrder, []uint8{uint8(0x12), uint8(0x34), uint8(0x56), uint8(0x78)})
+
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, TestDefaultByteOrder)
+
+	addressableOffset := uint32(0x1234)
+	ida := newIfdDataAllocator(addressableOffset)
+
+	childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, uint32(0))
+	log.PanicIf(err)
+
+	if childIfdBlock != nil {
+		t.Fatalf("no child-IFDs were expected to be allocated")
+	} else if bytes.Compare(b.Bytes(), []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x12, 0x34, 0x56, 0x78}) != 0 {
+		t.Fatalf("encoded tag-entry bytes not correct")
+	} else if ida.NextOffset() != addressableOffset {
+		t.Fatalf("allocation was done but not expected")
+	}
+}
+
+func Test_IfdByteEncoder_encodeTagToBytes_bytes_allocated(t *testing.T) {
+	ibe := NewIfdByteEncoder()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandardGps, TestDefaultByteOrder)
+
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, TestDefaultByteOrder)
+
+	addressableOffset := uint32(0x1234)
+	ida := newIfdDataAllocator(addressableOffset)
+
+	it, err := ti.Get(ib.ifdPath, uint16(0x0000))
+	log.PanicIf(err)
+
+	bt := NewStandardBuilderTag(IfdPathStandardGps, it, TestDefaultByteOrder, []uint8{uint8(0x12), uint8(0x34), uint8(0x56), uint8(0x78), uint8(0x9a)})
+
+	childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, uint32(0))
+	log.PanicIf(err)
+
+	if childIfdBlock != nil {
+		t.Fatalf("no child-IFDs were expected to be allocated (1)")
+	} else if bytes.Compare(b.Bytes(), []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x12, 0x34}) != 0 {
+		t.Fatalf("encoded tag-entry bytes not correct (1)")
+	} else if ida.NextOffset() != addressableOffset+uint32(5) {
+		t.Fatalf("allocation offset not expected (1)")
+	} else if bytes.Compare(ida.Bytes(), []byte{0x12, 0x34, 0x56, 0x78, 0x9A}) != 0 {
+		t.Fatalf("allocated data not correct (1)")
+	}
+
+	// Test that another allocation encodes to the new offset.
+
+	bt = NewStandardBuilderTag(IfdPathStandardGps, it, TestDefaultByteOrder, []uint8{uint8(0xbc), uint8(0xde), uint8(0xf0), uint8(0x12), uint8(0x34)})
+
+	childIfdBlock, err = ibe.encodeTagToBytes(ib, bt, bw, ida, uint32(0))
+	log.PanicIf(err)
+
+	if childIfdBlock != nil {
+		t.Fatalf("no child-IFDs were expected to be allocated (2)")
+	} else if bytes.Compare(b.Bytes(), []byte{
+		0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x12, 0x34, // Tag 1
+		0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x12, 0x39, // Tag 2
+	}) != 0 {
+		t.Fatalf("encoded tag-entry bytes not correct (2)")
+	} else if ida.NextOffset() != addressableOffset+uint32(10) {
+		t.Fatalf("allocation offset not expected (2)")
+	} else if bytes.Compare(ida.Bytes(), []byte{
+		0x12, 0x34, 0x56, 0x78, 0x9A,
+		0xbc, 0xde, 0xf0, 0x12, 0x34,
+	}) != 0 {
+		t.Fatalf("allocated data not correct (2)")
+	}
+}
+
+func Test_IfdByteEncoder_encodeTagToBytes_childIfd__withoutAllocate(t *testing.T) {
+	ibe := NewIfdByteEncoder()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, TestDefaultByteOrder)
+
+	addressableOffset := uint32(0x1234)
+	ida := newIfdDataAllocator(addressableOffset)
+
+	childIb := NewIfdBuilder(im, ti, IfdPathStandardExif, TestDefaultByteOrder)
+	tagValue := NewIfdBuilderTagValueFromIfdBuilder(childIb)
+	bt := NewChildIfdBuilderTag(IfdPathStandard, IfdExifId, tagValue)
+
+	nextIfdOffsetToWrite := uint32(0)
+	childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, nextIfdOffsetToWrite)
+	log.PanicIf(err)
+
+	if childIfdBlock != nil {
+		t.Fatalf("no child-IFDs were expected to be allocated")
+	} else if bytes.Compare(b.Bytes(), []byte{0x87, 0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00}) != 0 {
+		t.Fatalf("encoded tag-entry with child-IFD not correct")
+	} else if ida.NextOffset() != addressableOffset {
+		t.Fatalf("allocation offset not expected")
+	}
+}
+
+func Test_IfdByteEncoder_encodeTagToBytes_childIfd__withAllocate(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Test failure.")
+			panic(err)
+		}
+	}()
+
+	// Create a child IFD (represented by an IB instance) that we can allocate
+	// space for and then attach to a tag (which would normally be an entry,
+	// then, in a higher IFD).
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	childIb := NewIfdBuilder(im, ti, IfdPathStandardExif, TestDefaultByteOrder)
+
+	childIbTestTag := &BuilderTag{
+		ifdPath: IfdPathStandardExif,
+		tagId:   0x8822,
+		typeId:  TypeShort,
+		value:   NewIfdBuilderTagValueFromBytes([]byte{0x12, 0x34}),
+	}
+
+	childIb.Add(childIbTestTag)
+
+	// Formally compose the tag that refers to it.
+
+	tagValue := NewIfdBuilderTagValueFromIfdBuilder(childIb)
+	bt := NewChildIfdBuilderTag(IfdPathStandard, IfdExifId, tagValue)
+
+	// Encode the tag. Since we've actually provided an offset at which we can
+	// allocate data, the child-IFD will automatically be encoded, allocated,
+	// and installed into the allocated-data block (which will follow the IFD
+	// block/table in the file).
+
+	ibe := NewIfdByteEncoder()
+
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, TestDefaultByteOrder)
+
+	// addressableOffset is the offset of where large data can be allocated
+	// (which follows the IFD table/block). Large, in that it can't be stored
+	// in the table itself. Just used for arithmetic. This is just where the
+	// data for the current IFD can be written. It's not absolute for the EXIF
+	// data in general.
+	addressableOffset := uint32(0x1234)
+	ida := newIfdDataAllocator(addressableOffset)
+
+	// This is the offset of where the next IFD can be written in the EXIF byte
+	// stream. Just used for arithmetic.
+	nextIfdOffsetToWrite := uint32(2000)
+
+	childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, nextIfdOffsetToWrite)
+	log.PanicIf(err)
+
+	if ida.NextOffset() != addressableOffset {
+		t.Fatalf("IDA offset changed but no allocations where expected: (0x%02x)", ida.NextOffset())
+	}
+
+	tagBytes := b.Bytes()
+
+	if len(tagBytes) != 12 {
+		t.Fatalf("Tag not encoded to the right number of bytes: (%d)", len(tagBytes))
+	} else if len(childIfdBlock) != 18 {
+		t.Fatalf("Child IFD is not the right size: (%d)", len(childIfdBlock))
+	}
+
+	iteV, err := ParseOneTag(im, ti, fmt.Sprintf("%s%d", IfdPathStandard, 0), IfdPathStandard, TestDefaultByteOrder, tagBytes, false)
+	log.PanicIf(err)
+
+	if iteV.TagId != IfdExifId {
+		t.Fatalf("IFD first tag-ID not correct: (0x%02x)", iteV.TagId)
+	} else if iteV.TagIndex != 0 {
+		t.Fatalf("IFD first tag index not correct: (%d)", iteV.TagIndex)
+	} else if iteV.TagType != TypeLong {
+		t.Fatalf("IFD first tag type not correct: (%d)", iteV.TagType)
+	} else if iteV.UnitCount != 1 {
+		t.Fatalf("IFD first tag unit-count not correct: (%d)", iteV.UnitCount)
+	} else if iteV.ValueOffset != nextIfdOffsetToWrite {
+		t.Fatalf("IFD's child-IFD offset (as offset) is not correct: (%d) != (%d)", iteV.ValueOffset, nextIfdOffsetToWrite)
+	} else if bytes.Compare(iteV.RawValueOffset, []byte{0x0, 0x0, 0x07, 0xd0}) != 0 {
+		t.Fatalf("IFD's child-IFD offset (as raw bytes) is not correct: [%x]", iteV.RawValueOffset)
+	} else if iteV.ChildIfdPath != IfdPathStandardExif {
+		t.Fatalf("IFD first tag IFD-name name not correct: [%s]", iteV.ChildIfdPath)
+	} else if iteV.IfdPath != IfdPathStandard {
+		t.Fatalf("IFD first tag parent IFD not correct: %v", iteV.IfdPath)
+	}
+
+	// Validate the child's raw IFD bytes.
+
+	childNextIfdOffset, childEntries, err := ParseOneIfd(im, ti, "IFD0/Exif0", "IFD/Exif", TestDefaultByteOrder, childIfdBlock, nil, false)
+	log.PanicIf(err)
+
+	if childNextIfdOffset != uint32(0) {
+		t.Fatalf("Child IFD: Next IFD offset should be (0): (0x%08x)", childNextIfdOffset)
+	} else if len(childEntries) != 1 {
+		t.Fatalf("Child IFD: Expected exactly one entry: (%d)", len(childEntries))
+	}
+
+	ite := childEntries[0]
+
+	if ite.TagId != 0x8822 {
+		t.Fatalf("Child IFD first tag-ID not correct: (0x%02x)", ite.TagId)
+	} else if ite.TagIndex != 0 {
+		t.Fatalf("Child IFD first tag index not correct: (%d)", ite.TagIndex)
+	} else if ite.TagType != TypeShort {
+		t.Fatalf("Child IFD first tag type not correct: (%d)", ite.TagType)
+	} else if ite.UnitCount != 1 {
+		t.Fatalf("Child IFD first tag unit-count not correct: (%d)", ite.UnitCount)
+	} else if ite.ValueOffset != 0x12340000 {
+		t.Fatalf("Child IFD first tag value value (as offset) not correct: (0x%02x)", ite.ValueOffset)
+	} else if bytes.Compare(ite.RawValueOffset, []byte{0x12, 0x34, 0x0, 0x0}) != 0 {
+		t.Fatalf("Child IFD first tag value value (as raw bytes) not correct: [%v]", ite.RawValueOffset)
+	} else if ite.ChildIfdPath != "" {
+		t.Fatalf("Child IFD first tag IFD-name name not empty: [%s]", ite.ChildIfdPath)
+	} else if ite.IfdPath != IfdPathStandardExif {
+		t.Fatalf("Child IFD first tag parent IFD not correct: %v", ite.IfdPath)
+	}
+}
+
+func Test_IfdByteEncoder_encodeTagToBytes_simpleTag_allocate(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Test failure.")
+			panic(err)
+		}
+	}()
+
+	// Encode the tag. Since we've actually provided an offset at which we can
+	// allocate data, the child-IFD will automatically be encoded, allocated,
+	// and installed into the allocated-data block (which will follow the IFD
+	// block/table in the file).
+
+	ibe := NewIfdByteEncoder()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	it, err := ib.tagIndex.Get(ib.ifdPath, uint16(0x000b))
+	log.PanicIf(err)
+
+	valueString := "testvalue"
+	bt := NewStandardBuilderTag(IfdPathStandard, it, TestDefaultByteOrder, valueString)
+
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, TestDefaultByteOrder)
+
+	// addressableOffset is the offset of where large data can be allocated
+	// (which follows the IFD table/block). Large, in that it can't be stored
+	// in the table itself. Just used for arithmetic. This is just where the
+	// data for the current IFD can be written. It's not absolute for the EXIF
+	// data in general.
+	addressableOffset := uint32(0x1234)
+	ida := newIfdDataAllocator(addressableOffset)
+
+	childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, uint32(0))
+	log.PanicIf(err)
+
+	if ida.NextOffset() == addressableOffset {
+		t.Fatalf("IDA offset did not change even though there should've been an allocation.")
+	}
+
+	tagBytes := b.Bytes()
+
+	if len(tagBytes) != 12 {
+		t.Fatalf("Tag not encoded to the right number of bytes: (%d)", len(tagBytes))
+	} else if len(childIfdBlock) != 0 {
+		t.Fatalf("Child IFD not have been allocated.")
+	}
+
+	ite, err := ParseOneTag(im, ti, fmt.Sprintf("%s%d", IfdPathStandard, 0), IfdPathStandard, TestDefaultByteOrder, tagBytes, false)
+	log.PanicIf(err)
+
+	if ite.TagId != 0x000b {
+		t.Fatalf("Tag-ID not correct: (0x%02x)", ite.TagId)
+	} else if ite.TagIndex != 0 {
+		t.Fatalf("Tag index not correct: (%d)", ite.TagIndex)
+	} else if ite.TagType != TypeAscii {
+		t.Fatalf("Tag type not correct: (%d)", ite.TagType)
+	} else if ite.UnitCount != (uint32(len(valueString) + 1)) {
+		t.Fatalf("Tag unit-count not correct: (%d)", ite.UnitCount)
+	} else if ite.ValueOffset != addressableOffset {
+		t.Fatalf("Tag's value (as offset) is not correct: (%d) != (%d)", ite.ValueOffset, addressableOffset)
+	} else if bytes.Compare(ite.RawValueOffset, []byte{0x0, 0x0, 0x12, 0x34}) != 0 {
+		t.Fatalf("Tag's value (as raw bytes) is not correct: [%x]", ite.RawValueOffset)
+	} else if ite.ChildIfdPath != "" {
+		t.Fatalf("Tag's IFD-name should be empty: [%s]", ite.ChildIfdPath)
+	} else if ite.IfdPath != IfdPathStandard {
+		t.Fatalf("Tag's parent IFD is not correct: %v", ite.IfdPath)
+	}
+
+	expectedBuffer := bytes.NewBufferString(valueString)
+	expectedBuffer.Write([]byte{0x0})
+	expectedBytes := expectedBuffer.Bytes()
+
+	allocatedBytes := ida.Bytes()
+
+	if bytes.Compare(allocatedBytes, expectedBytes) != 0 {
+		t.Fatalf("Allocated bytes not correct: %v != %v", allocatedBytes, expectedBytes)
+	}
+}
+
+func Test_IfdByteEncoder_encodeIfdToBytes_simple(t *testing.T) {
+	ib := getExifSimpleTestIb()
+
+	// Write the byte stream.
+
+	ibe := NewIfdByteEncoder()
+
+	// addressableOffset is the offset of where large data can be allocated
+	// (which follows the IFD table/block). Large, in that it can't be stored
+	// in the table itself. Just used for arithmetic. This is just where the
+	// data for the current IFD can be written. It's not absolute for the EXIF
+	// data in general.
+	addressableOffset := uint32(0x1234)
+
+	tableAndAllocated, tableSize, allocatedDataSize, childIfdSizes, err := ibe.encodeIfdToBytes(ib, addressableOffset, uint32(0), false)
+	log.PanicIf(err)
+
+	expectedTableSize := ibe.TableSize(4)
+	if tableSize != expectedTableSize {
+		t.Fatalf("Table-size not the right size: (%d) != (%d)", tableSize, expectedTableSize)
+	} else if len(childIfdSizes) != 0 {
+		t.Fatalf("One or more child IFDs were allocated but shouldn't have been: (%d)", len(childIfdSizes))
+	}
+
+	// The ASCII value plus the rational size.
+	expectedAllocatedSize := 11 + 8
+
+	if int(allocatedDataSize) != expectedAllocatedSize {
+		t.Fatalf("Allocated data size not correct: (%d)", allocatedDataSize)
+	}
+
+	expectedIfdAndDataBytes := []byte{
+		// IFD table block.
+
+		// - Tag count
+		0x00, 0x04,
+
+		// - Tags
+		0x00, 0x0b, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x12, 0x34,
+		0x00, 0xff, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x11, 0x22, 0x00, 0x00,
+		0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x33, 0x44, 0x55, 0x66,
+		0x01, 0x3e, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x12, 0x3f,
+
+		// - Next IFD offset
+		0x00, 0x00, 0x00, 0x00,
+
+		// IFD data block.
+
+		// - The one ASCII value
+		0x61, 0x73, 0x63, 0x69, 0x69, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x00,
+
+		// - The one rational value
+		0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44,
+	}
+
+	if bytes.Compare(tableAndAllocated, expectedIfdAndDataBytes) != 0 {
+		t.Fatalf("IFD table and allocated data not correct: %v", DumpBytesClauseToString(tableAndAllocated))
+	}
+}
+
+func Test_IfdByteEncoder_encodeIfdToBytes_fullExif(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Test failure.")
+			panic(err)
+		}
+	}()
+
+	ib := getExifSimpleTestIb()
+
+	// Encode the IFD to a byte stream.
+
+	ibe := NewIfdByteEncoder()
+
+	// Run a simulation just to figure out the sizes.
+	_, tableSize, allocatedDataSize, _, err := ibe.encodeIfdToBytes(ib, uint32(0), uint32(0), false)
+	log.PanicIf(err)
+
+	addressableOffset := ExifDefaultFirstIfdOffset + tableSize
+	nextIfdOffsetToWrite := addressableOffset + allocatedDataSize
+
+	// Run the final encode now that we can correctly assign the offsets.
+	tableAndAllocated, _, _, _, err := ibe.encodeIfdToBytes(ib, addressableOffset, uint32(nextIfdOffsetToWrite), false)
+	log.PanicIf(err)
+
+	if len(tableAndAllocated) != (int(tableSize) + int(allocatedDataSize)) {
+		t.Fatalf("Table-and-data size doesn't match what was expected: (%d) != (%d + %d)", len(tableAndAllocated), tableSize, allocatedDataSize)
+	}
+
+	// Wrap the IFD in a formal EXIF block.
+
+	b := new(bytes.Buffer)
+
+	headerBytes, err := BuildExifHeader(TestDefaultByteOrder, ExifDefaultFirstIfdOffset)
+	log.PanicIf(err)
+
+	_, err = b.Write(headerBytes)
+	log.PanicIf(err)
+
+	_, err = b.Write(tableAndAllocated)
+	log.PanicIf(err)
+
+	// Now, try parsing it as EXIF data, making sure to resolve (read:
+	// dereference) the values (which will include the allocated ones).
+
+	exifData := b.Bytes()
+	validateExifSimpleTestIb(exifData, t)
+}
+
+func Test_IfdByteEncoder_EncodeToExifPayload(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Test failure.")
+			panic(err)
+		}
+	}()
+
+	ib := getExifSimpleTestIb()
+
+	// Encode the IFD to a byte stream.
+
+	ibe := NewIfdByteEncoder()
+
+	encodedIfds, err := ibe.EncodeToExifPayload(ib)
+	log.PanicIf(err)
+
+	// Wrap the IFD in a formal EXIF block.
+
+	b := new(bytes.Buffer)
+
+	headerBytes, err := BuildExifHeader(TestDefaultByteOrder, ExifDefaultFirstIfdOffset)
+	log.PanicIf(err)
+
+	_, err = b.Write(headerBytes)
+	log.PanicIf(err)
+
+	_, err = b.Write(encodedIfds)
+	log.PanicIf(err)
+
+	// Now, try parsing it as EXIF data, making sure to resolve (read:
+	// dereference) the values (which will include the allocated ones).
+
+	exifData := b.Bytes()
+	validateExifSimpleTestIb(exifData, t)
+}
+
+func Test_IfdByteEncoder_EncodeToExif(t *testing.T) {
+	ib := getExifSimpleTestIb()
+
+	// TODO(dustin): Do a child-IFD allocation in addition to the tag allocations.
+
+	ibe := NewIfdByteEncoder()
+
+	exifData, err := ibe.EncodeToExif(ib)
+	log.PanicIf(err)
+
+	validateExifSimpleTestIb(exifData, t)
+}
+
+func Test_IfdByteEncoder_EncodeToExif_WithChildAndSibling(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Test failure.")
+			panic(err)
+		}
+	}()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	err = ib.AddStandard(0x000b, "asciivalue")
+	log.PanicIf(err)
+
+	err = ib.AddStandard(0x00ff, []uint16{0x1122})
+	log.PanicIf(err)
+
+	// Add a child IB right in the middle.
+
+	childIb := NewIfdBuilder(im, ti, IfdPathStandardExif, TestDefaultByteOrder)
+
+	err = childIb.AddStandardWithName("ISOSpeedRatings", []uint16{0x1122})
+	log.PanicIf(err)
+
+	err = childIb.AddStandardWithName("ISOSpeed", []uint32{0x33445566})
+	log.PanicIf(err)
+
+	err = ib.AddChildIb(childIb)
+	log.PanicIf(err)
+
+	err = ib.AddStandard(0x0100, []uint32{0x33445566})
+	log.PanicIf(err)
+
+	// Add another child IB, just to ensure a little more punishment and make
+	// sure we're managing our allocation offsets correctly.
+
+	childIb2 := NewIfdBuilder(im, ti, IfdPathStandardGps, TestDefaultByteOrder)
+
+	err = childIb2.AddStandardWithName("GPSAltitudeRef", []uint8{0x11, 0x22})
+	log.PanicIf(err)
+
+	err = ib.AddChildIb(childIb2)
+	log.PanicIf(err)
+
+	err = ib.AddStandard(0x013e, []Rational{{Numerator: 0x11112222, Denominator: 0x33334444}})
+	log.PanicIf(err)
+
+	// Link to another IB (sibling relationship). The root/standard IFD may
+	// occur twice in some JPEGs (for thumbnail or FlashPix images).
+
+	nextIb := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	err = nextIb.AddStandard(0x0101, []uint32{0x11223344})
+	log.PanicIf(err)
+
+	err = nextIb.AddStandard(0x0102, []uint16{0x5566})
+	log.PanicIf(err)
+
+	ib.SetNextIb(nextIb)
+
+	// Encode.
+
+	ibe := NewIfdByteEncoder()
+
+	exifData, err := ibe.EncodeToExif(ib)
+	log.PanicIf(err)
+
+	// Parse.
+
+	_, index, err := Collect(im, ti, exifData)
+	log.PanicIf(err)
+
+	tagsDump := index.RootIfd.DumpTree()
+
+	actual := strings.Join(tagsDump, "\n")
+
+	expected :=
+		`> IFD [ROOT]->[IFD]:(0) TOP
+  - (0x000b)
+  - (0x00ff)
+  - (0x8769)
+  > IFD [IFD]->[IFD/Exif]:(0) TOP
+    - (0x8827)
+    - (0x8833)
+  < IFD [IFD]->[IFD/Exif]:(0) BOTTOM
+  - (0x0100)
+  - (0x8825)
+  > IFD [IFD]->[IFD/GPSInfo]:(0) TOP
+    - (0x0005)
+  < IFD [IFD]->[IFD/GPSInfo]:(0) BOTTOM
+  - (0x013e)
+< IFD [ROOT]->[IFD]:(0) BOTTOM
+* LINKING TO SIBLING IFD [IFD]:(1)
+> IFD [ROOT]->[IFD]:(1) TOP
+  - (0x0101)
+  - (0x0102)
+< IFD [ROOT]->[IFD]:(1) BOTTOM`
+
+	if actual != expected {
+		fmt.Printf("\n")
+
+		fmt.Printf("Actual:\n")
+		fmt.Printf("\n")
+		fmt.Printf("%s\n", actual)
+		fmt.Printf("\n")
+
+		fmt.Printf("Expected:\n")
+		fmt.Printf("\n")
+		fmt.Printf("%s\n", expected)
+		fmt.Printf("\n")
+
+		t.Fatalf("IFD hierarchy not correct.")
+	}
+}
+
+func ExampleIfdByteEncoder_EncodeToExif() {
+	// Construct an IFD.
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	err = ib.AddStandardWithName("ProcessingSoftware", "asciivalue")
+	log.PanicIf(err)
+
+	err = ib.AddStandardWithName("DotRange", []uint8{0x11})
+	log.PanicIf(err)
+
+	err = ib.AddStandardWithName("SubfileType", []uint16{0x2233})
+	log.PanicIf(err)
+
+	err = ib.AddStandardWithName("ImageWidth", []uint32{0x44556677})
+	log.PanicIf(err)
+
+	err = ib.AddStandardWithName("WhitePoint", []Rational{{Numerator: 0x11112222, Denominator: 0x33334444}})
+	log.PanicIf(err)
+
+	err = ib.AddStandardWithName("ShutterSpeedValue", []SignedRational{{Numerator: 0x11112222, Denominator: 0x33334444}})
+	log.PanicIf(err)
+
+	// Encode it.
+
+	ibe := NewIfdByteEncoder()
+
+	exifData, err := ibe.EncodeToExif(ib)
+	log.PanicIf(err)
+
+	// Parse it so we can see it.
+
+	_, index, err := Collect(im, ti, exifData)
+	log.PanicIf(err)
+
+	// addressableData is the byte-slice where the allocated data can be
+	// resolved (where position 0x0 will correlate with offset 0x0).
+	addressableData := exifData[ExifAddressableAreaStart:]
+
+	for i, e := range index.RootIfd.Entries {
+		value, err := e.Value(addressableData, TestDefaultByteOrder)
+		log.PanicIf(err)
+
+		fmt.Printf("%d: %s [%v]\n", i, e, value)
+	}
+
+	// Output:
+	//
+	// 0: IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x000b) TAG-TYPE=[ASCII] UNIT-COUNT=(11)> [asciivalue]
+	// 1: IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x0150) TAG-TYPE=[BYTE] UNIT-COUNT=(1)> [[17]]
+	// 2: IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x00ff) TAG-TYPE=[SHORT] UNIT-COUNT=(1)> [[8755]]
+	// 3: IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x0100) TAG-TYPE=[LONG] UNIT-COUNT=(1)> [[1146447479]]
+	// 4: IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x013e) TAG-TYPE=[RATIONAL] UNIT-COUNT=(1)> [[{286335522 858997828}]]
+	// 5: IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x9201) TAG-TYPE=[SRATIONAL] UNIT-COUNT=(1)> [[{286335522 858997828}]]
+}
diff --git a/v2/ifd_builder_test.go b/v2/ifd_builder_test.go
new file mode 100644
index 0000000..c5059b6
--- /dev/null
+++ b/v2/ifd_builder_test.go
@@ -0,0 +1,2110 @@
+package exif
+
+import (
+	"bytes"
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/dsoprea/go-logging"
+)
+
+func TestIfdBuilder_Add(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	originalBytes := []byte{0x11, 0x22, 0x33}
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x44,
+		value:   NewIfdBuilderTagValueFromBytes([]byte(originalBytes)),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	if ib.ifdPath != IfdPathStandard {
+		t.Fatalf("IFD name not correct.")
+	} else if ib.ifdTagId != 0 {
+		t.Fatalf("IFD tag-ID not correct.")
+	} else if ib.byteOrder != TestDefaultByteOrder {
+		t.Fatalf("IFD byte-order not correct.")
+	} else if len(ib.tags) != 4 {
+		t.Fatalf("IFD tag-count not correct.")
+	} else if ib.existingOffset != 0 {
+		t.Fatalf("IFD offset not correct.")
+	} else if ib.nextIb != nil {
+		t.Fatalf("Next-IFD not correct.")
+	}
+
+	tags := ib.Tags()
+
+	if tags[0].tagId != 0x11 {
+		t.Fatalf("tag (0) tag-ID not correct")
+	} else if bytes.Compare(tags[0].value.Bytes(), []byte("test string")) != 0 {
+		t.Fatalf("tag (0) value not correct")
+	}
+
+	if tags[1].tagId != 0x22 {
+		t.Fatalf("tag (1) tag-ID not correct")
+	} else if bytes.Compare(tags[1].value.Bytes(), []byte("test string2")) != 0 {
+		t.Fatalf("tag (1) value not correct")
+	}
+
+	if tags[2].tagId != 0x33 {
+		t.Fatalf("tag (2) tag-ID not correct")
+	} else if bytes.Compare(tags[2].value.Bytes(), []byte("test string3")) != 0 {
+		t.Fatalf("tag (2) value not correct")
+	}
+
+	if tags[3].tagId != 0x44 {
+		t.Fatalf("tag (3) tag-ID not correct")
+	} else if bytes.Compare(tags[3].value.Bytes(), originalBytes) != 0 {
+		t.Fatalf("tag (3) value not correct")
+	}
+}
+
+func TestIfdBuilder_SetNextIb(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	ib1 := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+	ib2 := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	if ib1.nextIb != nil {
+		t.Fatalf("Next-IFD for IB1 not initially terminal.")
+	}
+
+	err = ib1.SetNextIb(ib2)
+	log.PanicIf(err)
+
+	if ib1.nextIb != ib2 {
+		t.Fatalf("Next-IFD for IB1 not correct.")
+	} else if ib2.nextIb != nil {
+		t.Fatalf("Next-IFD for IB2 terminal.")
+	}
+}
+
+func TestIfdBuilder_AddChildIb(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	ibChild := NewIfdBuilder(im, ti, IfdPathStandardExif, TestDefaultByteOrder)
+	err = ib.AddChildIb(ibChild)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	if ib.tags[0].tagId != 0x11 {
+		t.Fatalf("first tag not correct")
+	} else if ib.tags[1].tagId != ibChild.ifdTagId {
+		t.Fatalf("second tag ID does not match child-IFD tag-ID: (0x%04x) != (0x%04x)", ib.tags[1].tagId, ibChild.ifdTagId)
+	} else if ib.tags[1].value.Ib() != ibChild {
+		t.Fatalf("second tagvalue does not match child-IFD")
+	} else if ib.tags[2].tagId != 0x22 {
+		t.Fatalf("third tag not correct")
+	}
+}
+
+func TestIfdBuilder_AddTagsFromExisting(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintError(err)
+
+			t.Fatalf("Test failure.")
+		}
+	}()
+
+	exifData := getExifSimpleTestIbBytes()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, exifData)
+	log.PanicIf(err)
+
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	err = ib.AddTagsFromExisting(index.RootIfd, nil, nil, nil)
+	log.PanicIf(err)
+
+	expected := []uint16{
+		0x000b,
+		0x00ff,
+		0x0100,
+		0x013e,
+	}
+
+	if len(ib.tags) != len(expected) {
+		t.Fatalf("Tag count not correct: (%d) != (%d)", len(ib.tags), len(expected))
+	}
+
+	for i, tag := range ib.tags {
+		if tag.tagId != expected[i] {
+			t.Fatalf("Tag (%d) not correct: (0x%04x) != (0x%04x)", i, tag.tagId, expected[i])
+		}
+	}
+}
+
+func TestIfdBuilder_AddTagsFromExisting__Includes(t *testing.T) {
+	exifData := getExifSimpleTestIbBytes()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, exifData)
+	log.PanicIf(err)
+
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	err = ib.AddTagsFromExisting(index.RootIfd, nil, []uint16{0x00ff}, nil)
+	log.PanicIf(err)
+
+	expected := []uint16{
+		0x00ff,
+	}
+
+	if len(ib.tags) != len(expected) {
+		t.Fatalf("Tag count not correct: (%d) != (%d)", len(ib.tags), len(expected))
+	}
+
+	for i, tag := range ib.tags {
+		if tag.tagId != expected[i] {
+			t.Fatalf("Tag (%d) not correct: (0x%04x) != (0x%04x)", i, tag.tagId, expected[i])
+		}
+	}
+}
+
+func TestIfdBuilder_AddTagsFromExisting__Excludes(t *testing.T) {
+	exifData := getExifSimpleTestIbBytes()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, exifData)
+	log.PanicIf(err)
+
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	err = ib.AddTagsFromExisting(index.RootIfd, nil, nil, []uint16{0xff})
+	log.PanicIf(err)
+
+	expected := []uint16{
+		0x000b,
+		0x0100,
+		0x013e,
+	}
+
+	if len(ib.tags) != len(expected) {
+		t.Fatalf("Tag count not correct: (%d) != (%d)", len(ib.tags), len(expected))
+	}
+
+	for i, tag := range ib.tags {
+		if tag.tagId != expected[i] {
+			t.Fatalf("Tag (%d) not correct: (0x%04x) != (0x%04x)", i, tag.tagId, expected[i])
+		}
+	}
+}
+
+func TestIfdBuilder_FindN__First_1(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	found, err := ib.FindN(0x11, 1)
+	log.PanicIf(err)
+
+	if len(found) != 1 {
+		log.Panicf("Exactly one result was not found: (%d)", len(found))
+	} else if found[0] != 0 {
+		log.Panicf("Result was not in the right place: (%d)", found[0])
+	}
+
+	tags := ib.Tags()
+	bt = tags[found[0]]
+
+	if bt.tagId != 0x11 {
+		log.Panicf("Found entry is not correct: (0x%04x)", bt.tagId)
+	}
+}
+
+func TestIfdBuilder_FindN__First_2_1Returned(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	found, err := ib.FindN(0x11, 2)
+	log.PanicIf(err)
+
+	if len(found) != 1 {
+		log.Panicf("Exactly one result was not found: (%d)", len(found))
+	} else if found[0] != 0 {
+		log.Panicf("Result was not in the right place: (%d)", found[0])
+	}
+
+	tags := ib.Tags()
+	bt = tags[found[0]]
+
+	if bt.tagId != 0x11 {
+		log.Panicf("Found entry is not correct: (0x%04x)", bt.tagId)
+	}
+}
+
+func TestIfdBuilder_FindN__First_2_2Returned(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string4")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string5")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	found, err := ib.FindN(0x11, 2)
+	log.PanicIf(err)
+
+	if len(found) != 2 {
+		log.Panicf("Exactly one result was not found: (%d)", len(found))
+	} else if found[0] != 0 {
+		log.Panicf("First result was not in the right place: (%d)", found[0])
+	} else if found[1] != 3 {
+		log.Panicf("Second result was not in the right place: (%d)", found[1])
+	}
+
+	tags := ib.Tags()
+
+	bt = tags[found[0]]
+	if bt.tagId != 0x11 || bytes.Compare(bt.value.Bytes(), []byte("test string")) != 0 {
+		log.Panicf("Found entry 0 is not correct: (0x%04x) [%s]", bt.tagId, bt.value)
+	}
+
+	bt = tags[found[1]]
+	if bt.tagId != 0x11 || bytes.Compare(bt.value.Bytes(), []byte("test string4")) != 0 {
+		log.Panicf("Found entry 1 is not correct: (0x%04x) [%s]", bt.tagId, bt.value)
+	}
+}
+
+func TestIfdBuilder_FindN__Middle_WithDuplicates(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string4")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string5")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string6")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	found, err := ib.FindN(0x33, 1)
+	log.PanicIf(err)
+
+	if len(found) != 1 {
+		log.Panicf("Exactly one result was not found: (%d)", len(found))
+	} else if found[0] != 2 {
+		log.Panicf("Result was not in the right place: (%d)", found[0])
+	}
+
+	tags := ib.Tags()
+	bt = tags[found[0]]
+
+	if bt.tagId != 0x33 {
+		log.Panicf("Found entry is not correct: (0x%04x)", bt.tagId)
+	}
+}
+
+func TestIfdBuilder_FindN__Middle_NoDuplicates(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string4")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	found, err := ib.FindN(0x33, 1)
+	log.PanicIf(err)
+
+	if len(found) != 1 {
+		log.Panicf("Exactly one result was not found: (%d)", len(found))
+	} else if found[0] != 2 {
+		log.Panicf("Result was not in the right place: (%d)", found[0])
+	}
+
+	tags := ib.Tags()
+	bt = tags[found[0]]
+
+	if bt.tagId != 0x33 {
+		log.Panicf("Found entry is not correct: (0x%04x)", bt.tagId)
+	}
+}
+
+func TestIfdBuilder_FindN__Miss(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	found, err := ib.FindN(0x11, 1)
+	log.PanicIf(err)
+
+	if len(found) != 0 {
+		t.Fatalf("Expected empty results.")
+	}
+}
+
+func TestIfdBuilder_Find__Hit(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string4")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	position, err := ib.Find(0x33)
+	log.PanicIf(err)
+
+	if position != 2 {
+		log.Panicf("Result was not in the right place: (%d)", position)
+	}
+
+	tags := ib.Tags()
+	bt = tags[position]
+
+	if bt.tagId != 0x33 {
+		log.Panicf("Found entry is not correct: (0x%04x)", bt.tagId)
+	}
+}
+
+func TestIfdBuilder_Find__Miss(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string4")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	_, err = ib.Find(0x99)
+	if err == nil {
+		t.Fatalf("Expected an error.")
+	} else if log.Is(err, ErrTagEntryNotFound) == false {
+		log.Panic(err)
+	}
+}
+
+func TestIfdBuilder_Replace(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	currentIds := make([]uint16, 3)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0x22, 0x33}, currentIds) == false {
+		t.Fatalf("Pre-replace tags are not correct.")
+	}
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x99,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string4")),
+	}
+
+	err = ib.Replace(0x22, bt)
+	log.PanicIf(err)
+
+	currentIds = make([]uint16, 3)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0x99, 0x33}, currentIds) == false {
+		t.Fatalf("Post-replace tags are not correct.")
+	}
+}
+
+func TestIfdBuilder_ReplaceN(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	currentIds := make([]uint16, 3)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0x22, 0x33}, currentIds) == false {
+		t.Fatalf("Pre-replace tags are not correct.")
+	}
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0xA9,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string4")),
+	}
+
+	err = ib.ReplaceAt(1, bt)
+	log.PanicIf(err)
+
+	currentIds = make([]uint16, 3)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0xA9, 0x33}, currentIds) == false {
+		t.Fatalf("Post-replace tags are not correct.")
+	}
+}
+
+func TestIfdBuilder_DeleteFirst(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string4")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	if len(ib.Tags()) != 4 {
+		t.Fatalf("Pre-delete tag count not correct.")
+	}
+
+	currentIds := make([]uint16, 4)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0x22, 0x22, 0x33}, currentIds) == false {
+		t.Fatalf("Pre-delete tags not correct.")
+	}
+
+	err = ib.DeleteFirst(0x22)
+	log.PanicIf(err)
+
+	if len(ib.Tags()) != 3 {
+		t.Fatalf("Post-delete (1) tag count not correct.")
+	}
+
+	currentIds = make([]uint16, 3)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0x22, 0x33}, currentIds) == false {
+		t.Fatalf("Post-delete (1) tags not correct.")
+	}
+
+	err = ib.DeleteFirst(0x22)
+	log.PanicIf(err)
+
+	if len(ib.Tags()) != 2 {
+		t.Fatalf("Post-delete (2) tag count not correct.")
+	}
+
+	currentIds = make([]uint16, 2)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0x33}, currentIds) == false {
+		t.Fatalf("Post-delete (2) tags not correct.")
+	}
+
+	err = ib.DeleteFirst(0x22)
+	if err == nil {
+		t.Fatalf("Expected an error.")
+	} else if log.Is(err, ErrTagEntryNotFound) == false {
+		log.Panic(err)
+	}
+}
+
+func TestIfdBuilder_DeleteN(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string4")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	if len(ib.Tags()) != 4 {
+		t.Fatalf("Pre-delete tag count not correct.")
+	}
+
+	currentIds := make([]uint16, 4)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0x22, 0x22, 0x33}, currentIds) == false {
+		t.Fatalf("Pre-delete tags not correct.")
+	}
+
+	err = ib.DeleteN(0x22, 1)
+	log.PanicIf(err)
+
+	if len(ib.Tags()) != 3 {
+		t.Fatalf("Post-delete (1) tag count not correct.")
+	}
+
+	currentIds = make([]uint16, 3)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0x22, 0x33}, currentIds) == false {
+		t.Fatalf("Post-delete (1) tags not correct.")
+	}
+
+	err = ib.DeleteN(0x22, 1)
+	log.PanicIf(err)
+
+	if len(ib.Tags()) != 2 {
+		t.Fatalf("Post-delete (2) tag count not correct.")
+	}
+
+	currentIds = make([]uint16, 2)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0x33}, currentIds) == false {
+		t.Fatalf("Post-delete (2) tags not correct.")
+	}
+
+	err = ib.DeleteN(0x22, 1)
+	if err == nil {
+		t.Fatalf("Expected an error.")
+	} else if log.Is(err, ErrTagEntryNotFound) == false {
+		log.Panic(err)
+	}
+}
+
+func TestIfdBuilder_DeleteN_Two(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string4")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	if len(ib.Tags()) != 4 {
+		t.Fatalf("Pre-delete tag count not correct.")
+	}
+
+	currentIds := make([]uint16, 4)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0x22, 0x22, 0x33}, currentIds) == false {
+		t.Fatalf("Pre-delete tags not correct.")
+	}
+
+	err = ib.DeleteN(0x22, 2)
+	log.PanicIf(err)
+
+	if len(ib.Tags()) != 2 {
+		t.Fatalf("Post-delete tag count not correct.")
+	}
+
+	currentIds = make([]uint16, 2)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0x33}, currentIds) == false {
+		t.Fatalf("Post-delete tags not correct.")
+	}
+
+	err = ib.DeleteFirst(0x22)
+	if err == nil {
+		t.Fatalf("Expected an error.")
+	} else if log.Is(err, ErrTagEntryNotFound) == false {
+		log.Panic(err)
+	}
+}
+
+func TestIfdBuilder_DeleteAll(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	bt := &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x11,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string2")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x22,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string3")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	bt = &BuilderTag{
+		ifdPath: IfdPathStandard,
+		typeId:  TypeByte,
+		tagId:   0x33,
+		value:   NewIfdBuilderTagValueFromBytes([]byte("test string4")),
+	}
+
+	err = ib.Add(bt)
+	log.PanicIf(err)
+
+	if len(ib.Tags()) != 4 {
+		t.Fatalf("Pre-delete tag count not correct.")
+	}
+
+	currentIds := make([]uint16, 4)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0x22, 0x22, 0x33}, currentIds) == false {
+		t.Fatalf("Pre-delete tags not correct.")
+	}
+
+	n, err := ib.DeleteAll(0x22)
+	log.PanicIf(err)
+
+	if n != 2 {
+		t.Fatalf("Returned delete tag count not correct.")
+	} else if len(ib.Tags()) != 2 {
+		t.Fatalf("Post-delete tag count not correct.")
+	}
+
+	currentIds = make([]uint16, 2)
+	for i, bt := range ib.Tags() {
+		currentIds[i] = bt.tagId
+	}
+
+	if reflect.DeepEqual([]uint16{0x11, 0x33}, currentIds) == false {
+		t.Fatalf("Post-delete tags not correct.")
+	}
+
+	err = ib.DeleteFirst(0x22)
+	if err == nil {
+		t.Fatalf("Expected an error.")
+	} else if log.Is(err, ErrTagEntryNotFound) == false {
+		log.Panic(err)
+	}
+}
+
+func TestIfdBuilder_CreateIfdBuilderFromExistingChain(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Test failure.")
+		}
+	}()
+
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	ib := NewIfdBuilderFromExistingChain(index.RootIfd, nil)
+
+	actual := ib.DumpToStrings()
+
+	expected := []string{
+		"IFD<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-INDEX=(0) IFD-TAG-ID=(0x0000) TAG=[0x0000]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(0) TAG=[0x010f]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(1) TAG=[0x0110]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(2) TAG=[0x0112]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(3) TAG=[0x011a]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(4) TAG=[0x011b]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(5) TAG=[0x0128]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(6) TAG=[0x0132]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(7) TAG=[0x013b]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(8) TAG=[0x0213]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(9) TAG=[0x8298]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[IFD/Exif] TAG-INDEX=(10) TAG=[0x8769]>",
+		"IFD<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-INDEX=(0) IFD-TAG-ID=(0x8769) TAG=[0x8769]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(0) TAG=[0x829a]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(1) TAG=[0x829d]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(2) TAG=[0x8822]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(3) TAG=[0x8827]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(4) TAG=[0x8830]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(5) TAG=[0x8832]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(6) TAG=[0x9000]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(7) TAG=[0x9003]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(8) TAG=[0x9004]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(9) TAG=[0x9101]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(10) TAG=[0x9201]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(11) TAG=[0x9202]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(12) TAG=[0x9204]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(13) TAG=[0x9207]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(14) TAG=[0x9209]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(15) TAG=[0x920a]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(16) TAG=[0x927c]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(17) TAG=[0x9286]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(18) TAG=[0x9290]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(19) TAG=[0x9291]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(20) TAG=[0x9292]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(21) TAG=[0xa000]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(22) TAG=[0xa001]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(23) TAG=[0xa002]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(24) TAG=[0xa003]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[IFD/Exif/Iop] TAG-INDEX=(25) TAG=[0xa005]>",
+		"IFD<PARENTS=[IFD->IFD/Exif] FQ-IFD-PATH=[IFD/Exif/Iop] IFD-INDEX=(0) IFD-TAG-ID=(0xa005) TAG=[0xa005]>",
+		"TAG<PARENTS=[IFD->IFD/Exif] FQ-IFD-PATH=[IFD/Exif/Iop] IFD-TAG-ID=(0xa005) CHILD-IFD=[] TAG-INDEX=(0) TAG=[0x0001]>",
+		"TAG<PARENTS=[IFD->IFD/Exif] FQ-IFD-PATH=[IFD/Exif/Iop] IFD-TAG-ID=(0xa005) CHILD-IFD=[] TAG-INDEX=(1) TAG=[0x0002]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(26) TAG=[0xa20e]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(27) TAG=[0xa20f]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(28) TAG=[0xa210]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(29) TAG=[0xa401]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(30) TAG=[0xa402]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(31) TAG=[0xa403]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(32) TAG=[0xa406]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(33) TAG=[0xa430]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(34) TAG=[0xa431]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(35) TAG=[0xa432]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(36) TAG=[0xa434]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[] TAG-INDEX=(37) TAG=[0xa435]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[IFD/GPSInfo] TAG-INDEX=(11) TAG=[0x8825]>",
+		"IFD<PARENTS=[IFD] FQ-IFD-PATH=[IFD/GPSInfo] IFD-INDEX=(0) IFD-TAG-ID=(0x8825) TAG=[0x8825]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/GPSInfo] IFD-TAG-ID=(0x8825) CHILD-IFD=[] TAG-INDEX=(0) TAG=[0x0000]>",
+		"IFD<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-INDEX=(1) IFD-TAG-ID=(0x0000) TAG=[0x0000]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(0) TAG=[0x0201]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(1) TAG=[0x0202]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(2) TAG=[0x0103]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(3) TAG=[0x011a]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(4) TAG=[0x011b]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[] TAG-INDEX=(5) TAG=[0x0128]>",
+	}
+
+	if reflect.DeepEqual(actual, expected) == false {
+		fmt.Printf("ACTUAL:\n%s\n\nEXPECTED:\n%s\n", strings.Join(actual, "\n"), strings.Join(expected, "\n"))
+		t.Fatalf("IB did not [correctly] duplicate the IFD structure.")
+	}
+}
+
+// TODO(dustin): !! Test with an actual GPS-attached image.
+
+func TestIfdBuilder_CreateIfdBuilderFromExistingChain_RealData(t *testing.T) {
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	// Decode from binary.
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, originalIndex, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	originalThumbnailData, err := originalIndex.RootIfd.NextIfd.Thumbnail()
+	log.PanicIf(err)
+
+	originalTags := originalIndex.RootIfd.DumpTags()
+
+	// Encode back to binary.
+
+	ibe := NewIfdByteEncoder()
+
+	rootIb := NewIfdBuilderFromExistingChain(originalIndex.RootIfd, nil)
+
+	updatedExif, err := ibe.EncodeToExif(rootIb)
+	log.PanicIf(err)
+
+	// Parse again.
+
+	_, recoveredIndex, err := Collect(im, ti, updatedExif)
+	log.PanicIf(err)
+
+	recoveredTags := recoveredIndex.RootIfd.DumpTags()
+
+	recoveredThumbnailData, err := recoveredIndex.RootIfd.NextIfd.Thumbnail()
+	log.PanicIf(err)
+
+	// Check the thumbnail.
+
+	if bytes.Compare(recoveredThumbnailData, originalThumbnailData) != 0 {
+		t.Fatalf("recovered thumbnail does not match original")
+	}
+
+	// Validate that all of the same IFDs were presented.
+
+	originalIfdTags := make([][2]interface{}, 0)
+	for _, ite := range originalTags {
+		if ite.ChildIfdPath != "" {
+			originalIfdTags = append(originalIfdTags, [2]interface{}{ite.IfdPath, ite.TagId})
+		}
+	}
+
+	recoveredIfdTags := make([][2]interface{}, 0)
+	for _, ite := range recoveredTags {
+		if ite.ChildIfdPath != "" {
+			recoveredIfdTags = append(recoveredIfdTags, [2]interface{}{ite.IfdPath, ite.TagId})
+		}
+	}
+
+	if reflect.DeepEqual(recoveredIfdTags, originalIfdTags) != true {
+		fmt.Printf("Original IFD tags:\n\n")
+
+		for i, x := range originalIfdTags {
+			fmt.Printf("  %02d %v\n", i, x)
+		}
+
+		fmt.Printf("\nRecovered IFD tags:\n\n")
+
+		for i, x := range recoveredIfdTags {
+			fmt.Printf("  %02d %v\n", i, x)
+		}
+
+		fmt.Printf("\n")
+
+		t.Fatalf("Recovered IFD tags are not correct.")
+	}
+
+	// Validate that all of the tags owned by the IFDs were presented. Note
+	// that the thumbnail tags are not kept but only produced on the fly, which
+	// is why we check it above.
+
+	if len(recoveredTags) != len(originalTags) {
+		t.Fatalf("Recovered tag-count does not match original.")
+	}
+
+	for i, recoveredIte := range recoveredTags {
+		if recoveredIte.ChildIfdPath != "" {
+			continue
+		}
+
+		originalIte := originalTags[i]
+
+		if recoveredIte.IfdPath != originalIte.IfdPath {
+			t.Fatalf("IfdIdentity not as expected: %s != %s  ITE=%s", recoveredIte.IfdPath, originalIte.IfdPath, recoveredIte)
+		} else if recoveredIte.TagId != originalIte.TagId {
+			t.Fatalf("Tag-ID not as expected: %d != %d  ITE=%s", recoveredIte.TagId, originalIte.TagId, recoveredIte)
+		} else if recoveredIte.TagType != originalIte.TagType {
+			t.Fatalf("Tag-type not as expected: %d != %d  ITE=%s", recoveredIte.TagType, originalIte.TagType, recoveredIte)
+		}
+
+		// TODO(dustin): We're always accessing the addressable-data using the root-IFD. It shouldn't matter, but we'd rather access it from our specific IFD.
+		originalValueBytes, err := originalIte.ValueBytes(originalIndex.RootIfd.addressableData, originalIndex.RootIfd.ByteOrder)
+		log.PanicIf(err)
+
+		recoveredValueBytes, err := recoveredIte.ValueBytes(recoveredIndex.RootIfd.addressableData, recoveredIndex.RootIfd.ByteOrder)
+		log.PanicIf(err)
+
+		if bytes.Compare(originalValueBytes, recoveredValueBytes) != 0 {
+			t.Fatalf("bytes of tag content not correct: %s != %s", originalIte, recoveredIte)
+		}
+	}
+}
+
+// func TestIfdBuilder_CreateIfdBuilderFromExistingChain_RealData_WithUpdate(t *testing.T) {
+// 	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+// 	log.PanicIf(err)
+
+// 	// Decode from binary.
+
+// 	ti := NewTagIndex()
+
+// 	_, originalIndex, err := Collect(im, ti, rawExif)
+// 	log.PanicIf(err)
+
+// 	originalThumbnailData, err := originalIndex.RootIfd.NextIfd.Thumbnail()
+// 	log.PanicIf(err)
+
+// 	originalTags := originalIndex.RootIfd.DumpTags()
+
+// 	// Encode back to binary.
+
+// 	ibe := NewIfdByteEncoder()
+
+// 	rootIb := NewIfdBuilderFromExistingChain(originalIndex.RootIfd, nil)
+
+// 	// Update a tag,.
+
+// 	exifBt, err := rootIb.FindTagWithName("ExifTag")
+// 	log.PanicIf(err)
+
+// 	ucBt, err := exifBt.value.Ib().FindTagWithName("UserComment")
+// 	log.PanicIf(err)
+
+// 	uc := TagUnknownType_9298_UserComment{
+// 		EncodingType:  TagUnknownType_9298_UserComment_Encoding_ASCII,
+// 		EncodingBytes: []byte("TEST COMMENT"),
+// 	}
+
+// 	err = ucBt.SetValue(rootIb.byteOrder, uc)
+// 	log.PanicIf(err)
+
+// 	// Encode.
+
+// 	updatedExif, err := ibe.EncodeToExif(rootIb)
+// 	log.PanicIf(err)
+
+// 	// Parse again.
+
+// 	_, recoveredIndex, err := Collect(im, ti, updatedExif)
+// 	log.PanicIf(err)
+
+// 	recoveredTags := recoveredIndex.RootIfd.DumpTags()
+
+// 	recoveredThumbnailData, err := recoveredIndex.RootIfd.NextIfd.Thumbnail()
+// 	log.PanicIf(err)
+
+// 	// Check the thumbnail.
+
+// 	if bytes.Compare(recoveredThumbnailData, originalThumbnailData) != 0 {
+// 		t.Fatalf("recovered thumbnail does not match original")
+// 	}
+
+// 	// Validate that all of the same IFDs were presented.
+
+// 	originalIfdTags := make([][2]interface{}, 0)
+// 	for _, ite := range originalTags {
+// 		if ite.ChildIfdPath != "" {
+// 			originalIfdTags = append(originalIfdTags, [2]interface{}{ite.IfdPath, ite.TagId})
+// 		}
+// 	}
+
+// 	recoveredIfdTags := make([][2]interface{}, 0)
+// 	for _, ite := range recoveredTags {
+// 		if ite.ChildIfdPath != "" {
+// 			recoveredIfdTags = append(recoveredIfdTags, [2]interface{}{ite.IfdPath, ite.TagId})
+// 		}
+// 	}
+
+// 	if reflect.DeepEqual(recoveredIfdTags, originalIfdTags) != true {
+// 		fmt.Printf("Original IFD tags:\n\n")
+
+// 		for i, x := range originalIfdTags {
+// 			fmt.Printf("  %02d %v\n", i, x)
+// 		}
+
+// 		fmt.Printf("\nRecovered IFD tags:\n\n")
+
+// 		for i, x := range recoveredIfdTags {
+// 			fmt.Printf("  %02d %v\n", i, x)
+// 		}
+
+// 		fmt.Printf("\n")
+
+// 		t.Fatalf("Recovered IFD tags are not correct.")
+// 	}
+
+// 	// Validate that all of the tags owned by the IFDs were presented. Note
+// 	// that the thumbnail tags are not kept but only produced on the fly, which
+// 	// is why we check it above.
+
+// 	if len(recoveredTags) != len(originalTags) {
+// 		t.Fatalf("Recovered tag-count does not match original.")
+// 	}
+
+// 	for i, recoveredIte := range recoveredTags {
+// 		if recoveredIte.ChildIfdPath != "" {
+// 			continue
+// 		}
+
+// 		originalIte := originalTags[i]
+
+// 		if recoveredIte.IfdPath != originalIte.IfdPath {
+// 			t.Fatalf("IfdIdentity not as expected: %s != %s  ITE=%s", recoveredIte.IfdPath, originalIte.IfdPath, recoveredIte)
+// 		} else if recoveredIte.TagId != originalIte.TagId {
+// 			t.Fatalf("Tag-ID not as expected: %d != %d  ITE=%s", recoveredIte.TagId, originalIte.TagId, recoveredIte)
+// 		} else if recoveredIte.TagType != originalIte.TagType {
+// 			t.Fatalf("Tag-type not as expected: %d != %d  ITE=%s", recoveredIte.TagType, originalIte.TagType, recoveredIte)
+// 		}
+
+// 		originalValueBytes, err := originalIte.ValueBytes(originalIndex.RootIfd.addressableData, originalIndex.RootIfd.ByteOrder)
+// 		log.PanicIf(err)
+
+// 		recoveredValueBytes, err := recoveredIte.ValueBytes(recoveredIndex.RootIfd.addressableData, recoveredIndex.RootIfd.ByteOrder)
+// 		log.PanicIf(err)
+
+// 		if recoveredIte.TagId == 0x9286 {
+// 			expectedValueBytes := make([]byte, 0)
+
+// 			expectedValueBytes = append(expectedValueBytes, []byte{'A', 'S', 'C', 'I', 'I', 0, 0, 0}...)
+// 			expectedValueBytes = append(expectedValueBytes, []byte("TEST COMMENT")...)
+
+// 			if bytes.Compare(recoveredValueBytes, expectedValueBytes) != 0 {
+// 				t.Fatalf("Recovered UserComment does not have the right value: %v != %v", recoveredValueBytes, expectedValueBytes)
+// 			}
+// 		} else if bytes.Compare(recoveredValueBytes, originalValueBytes) != 0 {
+// 			t.Fatalf("bytes of tag content not correct: %v != %v  ITE=%s", recoveredValueBytes, originalValueBytes, recoveredIte)
+// 		}
+// 	}
+// }
+
+func ExampleIfd_Thumbnail() {
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	thumbnailData, err := index.RootIfd.NextIfd.Thumbnail()
+	log.PanicIf(err)
+
+	thumbnailData = thumbnailData
+	// Output:
+}
+
+func ExampleBuilderTag_SetValue() {
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	// Create builder.
+
+	rootIb := NewIfdBuilderFromExistingChain(index.RootIfd, nil)
+
+	// Find tag to update.
+
+	exifBt, err := rootIb.FindTagWithName("ExifTag")
+	log.PanicIf(err)
+
+	ucBt, err := exifBt.value.Ib().FindTagWithName("UserComment")
+	log.PanicIf(err)
+
+	// Update the value. Since this is an "undefined"-type tag, we have to use
+	// its type-specific struct.
+
+	// TODO(dustin): !! Add an example for setting a non-unknown value, too.
+	uc := TagUnknownType_9298_UserComment{
+		EncodingType:  TagUnknownType_9298_UserComment_Encoding_ASCII,
+		EncodingBytes: []byte("TEST COMMENT"),
+	}
+
+	err = ucBt.SetValue(rootIb.byteOrder, uc)
+	log.PanicIf(err)
+
+	// Encode.
+
+	ibe := NewIfdByteEncoder()
+	updatedExif, err := ibe.EncodeToExif(rootIb)
+	log.PanicIf(err)
+
+	updatedExif = updatedExif
+	// Output:
+}
+
+// ExampleIfdBuilder_SetStandardWithName establishes a chain of `IfdBuilder`
+// structs from an existing chain of `Ifd` structs, navigates to the IB
+// representing IFD0, updates the ProcessingSoftware tag to a different value,
+// encodes down to a new EXIF block, reparses, and validates that the value for
+// that tag is what we set it to.
+func ExampleIfdBuilder_SetStandardWithName() {
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	// Boilerplate.
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	// Load current IFDs.
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	ib := NewIfdBuilderFromExistingChain(index.RootIfd, nil)
+
+	// Read the IFD whose tag we want to change.
+
+	// Standard:
+	// - "IFD0"
+	// - "IFD0/Exif0"
+	// - "IFD0/Exif0/Iop0"
+	// - "IFD0/GPSInfo0"
+	//
+	// If the numeric indices are not included, (0) is the default. Note that
+	// this isn't strictly necessary in our case since IFD0 is the first IFD anyway, but we're putting it here to show usage.
+	ifdPath := "IFD0"
+
+	childIb, err := GetOrCreateIbFromRootIb(ib, ifdPath)
+	log.PanicIf(err)
+
+	// There are a few functions that allow you to surgically change the tags in an
+	// IFD, but we're just gonna overwrite a tag that has an ASCII value.
+
+	tagName := "ProcessingSoftware"
+
+	err = childIb.SetStandardWithName(tagName, "alternative software")
+	log.PanicIf(err)
+
+	// Encode the in-memory representation back down to bytes.
+
+	ibe := NewIfdByteEncoder()
+
+	updatedRawExif, err := ibe.EncodeToExif(ib)
+	log.PanicIf(err)
+
+	// Reparse the EXIF to confirm that our value is there.
+
+	_, index, err = Collect(im, ti, updatedRawExif)
+	log.PanicIf(err)
+
+	// This isn't strictly necessary for the same reason as above, but it's here
+	// for documentation.
+	childIfd, err := FindIfdFromRootIfd(index.RootIfd, ifdPath)
+	log.PanicIf(err)
+
+	results, err := childIfd.FindTagWithName(tagName)
+	log.PanicIf(err)
+
+	for _, ite := range results {
+		value, err := childIfd.TagValue(ite)
+		log.PanicIf(err)
+
+		stringValue := value.(string)
+		fmt.Println(stringValue)
+	}
+
+	// Output:
+	// alternative software
+}
+
+func TestIfdBuilder_CreateIfdBuilderWithExistingIfd(t *testing.T) {
+	ti := NewTagIndex()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	mi, err := im.GetWithPath(IfdPathStandardGps)
+	log.PanicIf(err)
+
+	tagId := mi.TagId
+
+	parentIfd := &Ifd{
+		IfdPath:  IfdPathStandard,
+		tagIndex: ti,
+	}
+
+	ifd := &Ifd{
+		IfdPath:   IfdPathStandardGps,
+		ByteOrder: TestDefaultByteOrder,
+		Offset:    0x123,
+		ParentIfd: parentIfd,
+
+		ifdMapping: im,
+		tagIndex:   ti,
+	}
+
+	ib := NewIfdBuilderWithExistingIfd(ifd)
+
+	if ib.ifdPath != ifd.IfdPath {
+		t.Fatalf("IFD-name not correct.")
+	} else if ib.ifdTagId != tagId {
+		t.Fatalf("IFD tag-ID not correct.")
+	} else if ib.byteOrder != ifd.ByteOrder {
+		t.Fatalf("IFD byte-order not correct.")
+	} else if ib.existingOffset != ifd.Offset {
+		t.Fatalf("IFD offset not correct.")
+	}
+}
+
+func TestNewStandardBuilderTag__OneUnit(t *testing.T) {
+	ti := NewTagIndex()
+
+	it, err := ti.Get(IfdPathStandardExif, uint16(0x8833))
+	log.PanicIf(err)
+
+	bt := NewStandardBuilderTag(IfdPathStandardExif, it, TestDefaultByteOrder, []uint32{uint32(0x1234)})
+
+	if bt.ifdPath != IfdPathStandardExif {
+		t.Fatalf("II in BuilderTag not correct")
+	} else if bt.tagId != 0x8833 {
+		t.Fatalf("tag-ID not correct")
+	} else if bytes.Compare(bt.value.Bytes(), []byte{0x0, 0x0, 0x12, 0x34}) != 0 {
+		t.Fatalf("value not correct")
+	}
+}
+
+func TestNewStandardBuilderTag__TwoUnits(t *testing.T) {
+	ti := NewTagIndex()
+
+	it, err := ti.Get(IfdPathStandardExif, uint16(0x8833))
+	log.PanicIf(err)
+
+	bt := NewStandardBuilderTag(IfdPathStandardExif, it, TestDefaultByteOrder, []uint32{uint32(0x1234), uint32(0x5678)})
+
+	if bt.ifdPath != IfdPathStandardExif {
+		t.Fatalf("II in BuilderTag not correct")
+	} else if bt.tagId != 0x8833 {
+		t.Fatalf("tag-ID not correct")
+	} else if bytes.Compare(bt.value.Bytes(), []byte{
+		0x0, 0x0, 0x12, 0x34,
+		0x0, 0x0, 0x56, 0x78}) != 0 {
+		t.Fatalf("value not correct")
+	}
+}
+
+func TestIfdBuilder_AddStandardWithName(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	err = ib.AddStandardWithName("ProcessingSoftware", "some software")
+	log.PanicIf(err)
+
+	if len(ib.tags) != 1 {
+		t.Fatalf("Exactly one tag was not found: (%d)", len(ib.tags))
+	}
+
+	bt := ib.tags[0]
+
+	if bt.ifdPath != IfdPathStandard {
+		t.Fatalf("II not correct: %s", bt.ifdPath)
+	} else if bt.tagId != 0x000b {
+		t.Fatalf("Tag-ID not correct: (0x%04x)", bt.tagId)
+	}
+
+	s := string(bt.value.Bytes())
+
+	if s != "some software\000" {
+		t.Fatalf("Value not correct: (%d) [%s]", len(s), s)
+	}
+}
+
+func TestGetOrCreateIbFromRootIb__Noop(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	rootIb := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	ib, err := GetOrCreateIbFromRootIb(rootIb, "IFD")
+	log.PanicIf(err)
+
+	if ib != rootIb {
+		t.Fatalf("Expected same IB back from no-op get-or-create.")
+	} else if ib.nextIb != nil {
+		t.Fatalf("Expected no siblings on IB from no-op get-or-create.")
+	} else if len(ib.tags) != 0 {
+		t.Fatalf("Expected no new tags on IB from no-op get-or-create.")
+	}
+}
+
+func TestGetOrCreateIbFromRootIb__FqNoop(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	rootIb := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	ib, err := GetOrCreateIbFromRootIb(rootIb, "IFD0")
+	log.PanicIf(err)
+
+	if ib != rootIb {
+		t.Fatalf("Expected same IB back from no-op get-or-create.")
+	} else if ib.nextIb != nil {
+		t.Fatalf("Expected no siblings on IB from no-op get-or-create.")
+	} else if len(ib.tags) != 0 {
+		t.Fatalf("Expected no new tags on IB from no-op get-or-create.")
+	}
+}
+
+func TestGetOrCreateIbFromRootIb_InvalidChild(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	rootIb := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	_, err = GetOrCreateIbFromRootIb(rootIb, "IFD/Invalid")
+	if err == nil {
+		t.Fatalf("Expected failure for invalid IFD child in IB get-or-create.")
+	} else if err.Error() != "ifd child with name [Invalid] not registered: [IFD/Invalid]" {
+		log.Panic(err)
+	}
+}
+
+func TestGetOrCreateIbFromRootIb__Child(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Test failure.")
+		}
+	}()
+
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+	rootIb := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
+
+	lines := rootIb.DumpToStrings()
+	expected := []string{
+		"IFD<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-INDEX=(0) IFD-TAG-ID=(0x0000) TAG=[0x0000]>",
+	}
+
+	if reflect.DeepEqual(lines, expected) != true {
+		fmt.Printf("ACTUAL:\n")
+		fmt.Printf("\n")
+
+		for i, line := range lines {
+			fmt.Printf("%d: %s\n", i, line)
+		}
+
+		fmt.Printf("\n")
+
+		fmt.Printf("EXPECTED:\n")
+		fmt.Printf("\n")
+
+		for i, line := range expected {
+			fmt.Printf("%d: %s\n", i, line)
+		}
+
+		fmt.Printf("\n")
+
+		t.Fatalf("Constructed IFDs not correct.")
+	}
+
+	ib, err := GetOrCreateIbFromRootIb(rootIb, "IFD/Exif")
+	log.PanicIf(err)
+
+	if ib.fqIfdPath != "IFD/Exif" {
+		t.Fatalf("Returned IB does not have the expected path (IFD/Exif).")
+	}
+
+	lines = rootIb.DumpToStrings()
+	expected = []string{
+		"IFD<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-INDEX=(0) IFD-TAG-ID=(0x0000) TAG=[0x0000]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[IFD/Exif] TAG-INDEX=(0) TAG=[0x8769]>",
+		"IFD<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-INDEX=(0) IFD-TAG-ID=(0x8769) TAG=[0x8769]>",
+	}
+
+	if reflect.DeepEqual(lines, expected) != true {
+		fmt.Printf("ACTUAL:\n")
+		fmt.Printf("\n")
+
+		for i, line := range lines {
+			fmt.Printf("%d: %s\n", i, line)
+		}
+
+		fmt.Printf("\n")
+
+		fmt.Printf("EXPECTED:\n")
+		fmt.Printf("\n")
+
+		for i, line := range expected {
+			fmt.Printf("%d: %s\n", i, line)
+		}
+
+		fmt.Printf("\n")
+
+		t.Fatalf("Constructed IFDs not correct.")
+	}
+
+	ib, err = GetOrCreateIbFromRootIb(rootIb, "IFD0/Exif/Iop")
+	log.PanicIf(err)
+
+	if ib.fqIfdPath != "IFD/Exif/Iop" {
+		t.Fatalf("Returned IB does not have the expected path (IFD/Exif/Iop).")
+	}
+
+	lines = rootIb.DumpToStrings()
+	expected = []string{
+		"IFD<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-INDEX=(0) IFD-TAG-ID=(0x0000) TAG=[0x0000]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[IFD/Exif] TAG-INDEX=(0) TAG=[0x8769]>",
+		"IFD<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-INDEX=(0) IFD-TAG-ID=(0x8769) TAG=[0x8769]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[IFD/Exif/Iop] TAG-INDEX=(0) TAG=[0xa005]>",
+		"IFD<PARENTS=[IFD->IFD/Exif] FQ-IFD-PATH=[IFD/Exif/Iop] IFD-INDEX=(0) IFD-TAG-ID=(0xa005) TAG=[0xa005]>",
+	}
+
+	if reflect.DeepEqual(lines, expected) != true {
+		fmt.Printf("ACTUAL:\n")
+		fmt.Printf("\n")
+
+		for i, line := range lines {
+			fmt.Printf("%d: %s\n", i, line)
+		}
+
+		fmt.Printf("\n")
+
+		fmt.Printf("EXPECTED:\n")
+		fmt.Printf("\n")
+
+		for i, line := range expected {
+			fmt.Printf("%d: %s\n", i, line)
+		}
+
+		fmt.Printf("\n")
+
+		t.Fatalf("Constructed IFDs not correct.")
+	}
+
+	ib, err = GetOrCreateIbFromRootIb(rootIb, "IFD1")
+	log.PanicIf(err)
+
+	if ib.fqIfdPath != "IFD1" {
+		t.Fatalf("Returned IB does not have the expected path (IFD1).")
+	}
+
+	lines = rootIb.DumpToStrings()
+	expected = []string{
+		"IFD<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-INDEX=(0) IFD-TAG-ID=(0x0000) TAG=[0x0000]>",
+		"TAG<PARENTS=[] FQ-IFD-PATH=[IFD] IFD-TAG-ID=(0x0000) CHILD-IFD=[IFD/Exif] TAG-INDEX=(0) TAG=[0x8769]>",
+		"IFD<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-INDEX=(0) IFD-TAG-ID=(0x8769) TAG=[0x8769]>",
+		"TAG<PARENTS=[IFD] FQ-IFD-PATH=[IFD/Exif] IFD-TAG-ID=(0x8769) CHILD-IFD=[IFD/Exif/Iop] TAG-INDEX=(0) TAG=[0xa005]>",
+		"IFD<PARENTS=[IFD->IFD/Exif] FQ-IFD-PATH=[IFD/Exif/Iop] IFD-INDEX=(0) IFD-TAG-ID=(0xa005) TAG=[0xa005]>",
+		"IFD<PARENTS=[] FQ-IFD-PATH=[IFD1] IFD-INDEX=(1) IFD-TAG-ID=(0x0000) TAG=[0x0000]>",
+	}
+
+	if reflect.DeepEqual(lines, expected) != true {
+		fmt.Printf("ACTUAL:\n")
+		fmt.Printf("\n")
+
+		for i, line := range lines {
+			fmt.Printf("%d: %s\n", i, line)
+		}
+
+		fmt.Printf("\n")
+
+		fmt.Printf("EXPECTED:\n")
+		fmt.Printf("\n")
+
+		for i, line := range expected {
+			fmt.Printf("%d: %s\n", i, line)
+		}
+
+		fmt.Printf("\n")
+
+		t.Fatalf("Constructed IFDs not correct.")
+	}
+}
diff --git a/v2/ifd_enumerate.go b/v2/ifd_enumerate.go
new file mode 100644
index 0000000..317e847
--- /dev/null
+++ b/v2/ifd_enumerate.go
@@ -0,0 +1,1356 @@
+package exif
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+
+	"encoding/binary"
+
+	"github.com/dsoprea/go-logging"
+)
+
+var (
+	ifdEnumerateLogger = log.NewLogger("exifjpeg.ifd")
+)
+
+var (
+	ErrNoThumbnail     = errors.New("no thumbnail")
+	ErrNoGpsTags       = errors.New("no gps tags")
+	ErrTagTypeNotValid = errors.New("tag type invalid")
+)
+
+var (
+	ValidGpsVersions = [][4]byte{
+		{2, 2, 0, 0},
+
+		// Suddenly appeared at the default in 2.31: https://home.jeita.or.jp/tsc/std-pdf/CP-3451D.pdf
+		//
+		// Note that the presence of 2.3.0.0 doesn't seem to guarantee
+		// coordinates. In some cases, we seen just the following:
+		//
+		// GPS Tag Version     |2.3.0.0
+		// GPS Receiver Status |V
+		// Geodetic Survey Data|WGS-84
+		// GPS Differential Cor|0
+		//
+		{2, 3, 0, 0},
+	}
+)
+
+// IfdTagEnumerator knows how to decode an IFD and all of the tags it
+// describes.
+//
+// The IFDs and the actual values can float throughout the EXIF block, but the
+// IFD itself is just a minor header followed by a set of repeating,
+// statically-sized records. So, the tags (though notnecessarily their values)
+// are fairly simple to enumerate.
+type IfdTagEnumerator struct {
+	byteOrder       binary.ByteOrder
+	addressableData []byte
+	ifdOffset       uint32
+	buffer          *bytes.Buffer
+}
+
+func NewIfdTagEnumerator(addressableData []byte, byteOrder binary.ByteOrder, ifdOffset uint32) (ite *IfdTagEnumerator) {
+	ite = &IfdTagEnumerator{
+		addressableData: addressableData,
+		byteOrder:       byteOrder,
+		buffer:          bytes.NewBuffer(addressableData[ifdOffset:]),
+	}
+
+	return ite
+}
+
+// getUint16 reads a uint16 and advances both our current and our current
+// accumulator (which allows us to know how far to seek to the beginning of the
+// next IFD when it's time to jump).
+func (ife *IfdTagEnumerator) getUint16() (value uint16, raw []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	needBytes := 2
+	offset := 0
+	raw = make([]byte, needBytes)
+
+	for offset < needBytes {
+		n, err := ife.buffer.Read(raw[offset:])
+		log.PanicIf(err)
+
+		offset += n
+	}
+
+	value = ife.byteOrder.Uint16(raw)
+
+	return value, raw, nil
+}
+
+// getUint32 reads a uint32 and advances both our current and our current
+// accumulator (which allows us to know how far to seek to the beginning of the
+// next IFD when it's time to jump).
+func (ife *IfdTagEnumerator) getUint32() (value uint32, raw []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	needBytes := 4
+	offset := 0
+	raw = make([]byte, needBytes)
+
+	for offset < needBytes {
+		n, err := ife.buffer.Read(raw[offset:])
+		log.PanicIf(err)
+
+		offset += n
+	}
+
+	value = ife.byteOrder.Uint32(raw)
+
+	return value, raw, nil
+}
+
+type IfdEnumerate struct {
+	exifData      []byte
+	buffer        *bytes.Buffer
+	byteOrder     binary.ByteOrder
+	currentOffset uint32
+	tagIndex      *TagIndex
+	ifdMapping    *IfdMapping
+}
+
+func NewIfdEnumerate(ifdMapping *IfdMapping, tagIndex *TagIndex, exifData []byte, byteOrder binary.ByteOrder) *IfdEnumerate {
+	return &IfdEnumerate{
+		exifData:   exifData,
+		buffer:     bytes.NewBuffer(exifData),
+		byteOrder:  byteOrder,
+		ifdMapping: ifdMapping,
+		tagIndex:   tagIndex,
+	}
+}
+
+func (ie *IfdEnumerate) getTagEnumerator(ifdOffset uint32) (ite *IfdTagEnumerator) {
+	ite = NewIfdTagEnumerator(
+		ie.exifData[ExifAddressableAreaStart:],
+		ie.byteOrder,
+		ifdOffset)
+
+	return ite
+}
+
+func (ie *IfdEnumerate) parseTag(fqIfdPath string, tagPosition int, ite *IfdTagEnumerator, resolveValue bool) (tag *IfdTagEntry, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	tagId, _, err := ite.getUint16()
+	log.PanicIf(err)
+
+	tagTypeRaw, _, err := ite.getUint16()
+	log.PanicIf(err)
+
+	tagType := TagTypePrimitive(tagTypeRaw)
+
+	unitCount, _, err := ite.getUint32()
+	log.PanicIf(err)
+
+	valueOffset, rawValueOffset, err := ite.getUint32()
+	log.PanicIf(err)
+
+	if _, found := TypeNames[tagType]; found == false {
+		log.Panic(ErrTagTypeNotValid)
+	}
+
+	ifdPath, err := ie.ifdMapping.StripPathPhraseIndices(fqIfdPath)
+	log.PanicIf(err)
+
+	tag = &IfdTagEntry{
+		IfdPath:        ifdPath,
+		TagId:          tagId,
+		TagIndex:       tagPosition,
+		TagType:        tagType,
+		UnitCount:      unitCount,
+		ValueOffset:    valueOffset,
+		RawValueOffset: rawValueOffset,
+	}
+
+	if resolveValue == true {
+		value, isUnhandledUnknown, err := ie.resolveTagValue(tag)
+		log.PanicIf(err)
+
+		tag.value = value
+		tag.isUnhandledUnknown = isUnhandledUnknown
+	}
+
+	// If it's an IFD but not a standard one, it'll just be seen as a LONG
+	// (the standard IFD tag type), later, unless we skip it because it's
+	// [likely] not even in the standard list of known tags.
+	mi, err := ie.ifdMapping.GetChild(ifdPath, tagId)
+	if err == nil {
+		tag.ChildIfdName = mi.Name
+		tag.ChildIfdPath = mi.PathPhrase()
+		tag.ChildFqIfdPath = fmt.Sprintf("%s/%s", fqIfdPath, mi.Name)
+
+		// We also need to set `tag.ChildFqIfdPath` but can't do it here
+		// because we don't have the IFD index.
+	} else if log.Is(err, ErrChildIfdNotMapped) == false {
+		log.Panic(err)
+	}
+
+	return tag, nil
+}
+
+func (ie *IfdEnumerate) GetValueContext(ite *IfdTagEntry) *ValueContext {
+
+	// TODO(dustin): Add test
+
+	addressableData := ie.exifData[ExifAddressableAreaStart:]
+
+	return newValueContextFromTag(
+		ite,
+		addressableData,
+		ie.byteOrder)
+}
+
+func (ie *IfdEnumerate) resolveTagValue(ite *IfdTagEntry) (valueBytes []byte, isUnhandledUnknown bool, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	addressableData := ie.exifData[ExifAddressableAreaStart:]
+
+	// Return the exact bytes of the unknown-type value. Returning a string
+	// (`ValueString`) is easy because we can just pass everything to
+	// `Sprintf()`. Returning the raw, typed value (`Value`) is easy
+	// (obviously). However, here, in order to produce the list of bytes, we
+	// need to coerce whatever `Undefined()` returns.
+	if ite.TagType == TypeUndefined {
+		valueContext := ie.GetValueContext(ite)
+
+		value, err := valueContext.Undefined()
+		if err != nil {
+			if err == ErrUnhandledUnknownTypedTag {
+				valueBytes = []byte(UnparseableUnknownTagValuePlaceholder)
+				return valueBytes, true, nil
+			}
+
+			log.Panic(err)
+		} else {
+			switch value.(type) {
+			case []byte:
+				return value.([]byte), false, nil
+			case TagUnknownType_UnknownValue:
+				b := []byte(value.(TagUnknownType_UnknownValue))
+				return b, false, nil
+			case string:
+				return []byte(value.(string)), false, nil
+			case UnknownTagValue:
+				valueBytes, err := value.(UnknownTagValue).ValueBytes()
+				log.PanicIf(err)
+
+				return valueBytes, false, nil
+			default:
+				// TODO(dustin): !! Finish translating the rest of the types (make reusable and replace into other similar implementations?)
+				log.Panicf("can not produce bytes for unknown-type tag (0x%04x) (1): [%s]", ite.TagId, reflect.TypeOf(value))
+			}
+		}
+	} else {
+		originalType := NewTagType(ite.TagType, ie.byteOrder)
+		byteCount := uint32(originalType.Type().Size()) * ite.UnitCount
+
+		tt := NewTagType(TypeByte, ie.byteOrder)
+
+		if tt.valueIsEmbedded(byteCount) == true {
+			iteLogger.Debugf(nil, "Reading BYTE value (ITE; embedded).")
+
+			// In this case, the bytes normally used for the offset are actually
+			// data.
+			valueBytes, err = tt.ParseBytes(ite.RawValueOffset, byteCount)
+			log.PanicIf(err)
+		} else {
+			iteLogger.Debugf(nil, "Reading BYTE value (ITE; at offset).")
+
+			valueBytes, err = tt.ParseBytes(addressableData[ite.ValueOffset:], byteCount)
+			log.PanicIf(err)
+		}
+	}
+
+	return valueBytes, false, nil
+}
+
+// RawTagVisitorPtr is an optional callback that can get hit for every tag we parse
+// through. `addressableData` is the byte array startign after the EXIF header
+// (where the offsets of all IFDs and values are calculated from).
+//
+// This was reimplemented as an interface to allow for simpler change management
+// in the future.
+type RawTagWalk interface {
+	Visit(fqIfdPath string, ifdIndex int, tagId uint16, tagType TagType, valueContext *ValueContext) (err error)
+}
+
+type RawTagWalkLegacyWrapper struct {
+	legacyVisitor RawTagVisitor
+}
+
+func (rtwlw RawTagWalkLegacyWrapper) Visit(fqIfdPath string, ifdIndex int, tagId uint16, tagType TagType, valueContext *ValueContext) (err error) {
+	return rtwlw.legacyVisitor(fqIfdPath, ifdIndex, tagId, tagType, *valueContext)
+}
+
+// RawTagVisitor is an optional callback that can get hit for every tag we parse
+// through. `addressableData` is the byte array startign after the EXIF header
+// (where the offsets of all IFDs and values are calculated from).
+//
+// DEPRECATED(dustin): Use a RawTagWalk instead.
+type RawTagVisitor func(fqIfdPath string, ifdIndex int, tagId uint16, tagType TagType, valueContext ValueContext) (err error)
+
+// ParseIfd decodes the IFD block that we're currently sitting on the first
+// byte of.
+func (ie *IfdEnumerate) ParseIfd(fqIfdPath string, ifdIndex int, ite *IfdTagEnumerator, visitor interface{}, doDescend bool, resolveValues bool) (nextIfdOffset uint32, entries []*IfdTagEntry, thumbnailData []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	var visitorWrapper RawTagWalk
+
+	if visitor != nil {
+		var ok bool
+
+		visitorWrapper, ok = visitor.(RawTagWalk)
+		if ok == false {
+			// Legacy usage.
+
+			// `ok` can be `true` but `legacyVisitor` can still be `nil` (when
+			// passed as nil).
+			if legacyVisitor, ok := visitor.(RawTagVisitor); ok == true && legacyVisitor != nil {
+				visitorWrapper = RawTagWalkLegacyWrapper{
+					legacyVisitor: legacyVisitor,
+				}
+			}
+		}
+	}
+
+	tagCount, _, err := ite.getUint16()
+	log.PanicIf(err)
+
+	ifdEnumerateLogger.Debugf(nil, "Current IFD tag-count: (%d)", tagCount)
+
+	entries = make([]*IfdTagEntry, 0)
+
+	var iteThumbnailOffset *IfdTagEntry
+	var iteThumbnailSize *IfdTagEntry
+
+	for i := 0; i < int(tagCount); i++ {
+		tag, err := ie.parseTag(fqIfdPath, i, ite, resolveValues)
+		if err != nil {
+			if log.Is(err, ErrTagTypeNotValid) == true {
+				ifdEnumerateLogger.Warningf(nil, "Tag in IFD [%s] at position (%d) has invalid type and will be skipped.", fqIfdPath, i)
+				continue
+			}
+
+			log.Panic(err)
+		}
+
+		if tag.TagId == ThumbnailOffsetTagId {
+			iteThumbnailOffset = tag
+
+			continue
+		} else if tag.TagId == ThumbnailSizeTagId {
+			iteThumbnailSize = tag
+			continue
+		}
+
+		if visitorWrapper != nil {
+			tt := NewTagType(tag.TagType, ie.byteOrder)
+
+			valueContext := ie.GetValueContext(tag)
+
+			err := visitorWrapper.Visit(fqIfdPath, ifdIndex, tag.TagId, tt, valueContext)
+			log.PanicIf(err)
+		}
+
+		// If it's an IFD but not a standard one, it'll just be seen as a LONG
+		// (the standard IFD tag type), later, unless we skip it because it's
+		// [likely] not even in the standard list of known tags.
+		if tag.ChildIfdPath != "" {
+			if doDescend == true {
+				ifdEnumerateLogger.Debugf(nil, "Descending to IFD [%s].", tag.ChildIfdPath)
+
+				err := ie.scan(tag.ChildFqIfdPath, tag.ValueOffset, visitor, resolveValues)
+				log.PanicIf(err)
+			}
+		}
+
+		entries = append(entries, tag)
+	}
+
+	if iteThumbnailOffset != nil && iteThumbnailSize != nil {
+		thumbnailData, err = ie.parseThumbnail(iteThumbnailOffset, iteThumbnailSize)
+		log.PanicIf(err)
+	}
+
+	nextIfdOffset, _, err = ite.getUint32()
+	log.PanicIf(err)
+
+	ifdEnumerateLogger.Debugf(nil, "Next IFD at offset: (%08x)", nextIfdOffset)
+
+	return nextIfdOffset, entries, thumbnailData, nil
+}
+
+func (ie *IfdEnumerate) parseThumbnail(offsetIte, lengthIte *IfdTagEntry) (thumbnailData []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	addressableData := ie.exifData[ExifAddressableAreaStart:]
+
+	vRaw, err := lengthIte.Value(addressableData, ie.byteOrder)
+	log.PanicIf(err)
+
+	vList := vRaw.([]uint32)
+	if len(vList) != 1 {
+		log.Panicf("not exactly one long: (%d)", len(vList))
+	}
+
+	length := vList[0]
+
+	// The tag is official a LONG type, but it's actually an offset to a blob of bytes.
+	offsetIte.TagType = TypeByte
+	offsetIte.UnitCount = length
+
+	thumbnailData, err = offsetIte.ValueBytes(addressableData, ie.byteOrder)
+	log.PanicIf(err)
+
+	return thumbnailData, nil
+}
+
+// Scan enumerates the different EXIF's IFD blocks.
+func (ie *IfdEnumerate) scan(fqIfdName string, ifdOffset uint32, visitor interface{}, resolveValues bool) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	for ifdIndex := 0; ; ifdIndex++ {
+		ifdEnumerateLogger.Debugf(nil, "Parsing IFD [%s] (%d) at offset (%04x).", fqIfdName, ifdIndex, ifdOffset)
+		ite := ie.getTagEnumerator(ifdOffset)
+
+		nextIfdOffset, _, _, err := ie.ParseIfd(fqIfdName, ifdIndex, ite, visitor, true, resolveValues)
+		log.PanicIf(err)
+
+		if nextIfdOffset == 0 {
+			break
+		}
+
+		ifdOffset = nextIfdOffset
+	}
+
+	return nil
+}
+
+// Scan enumerates the different EXIF blocks (called IFDs). `rootIfdName` will
+// be "IFD" in the TIFF standard.
+func (ie *IfdEnumerate) Scan(rootIfdName string, ifdOffset uint32, visitor RawTagVisitor, resolveValue bool) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	err = ie.scan(rootIfdName, ifdOffset, visitor, resolveValue)
+	log.PanicIf(err)
+
+	return nil
+}
+
+// Ifd represents a single parsed IFD.
+type Ifd struct {
+
+	// TODO(dustin): !! Why are all of these public? Privatize them and then add NextIfd().
+
+	// This is just for convenience, just so that we can easily get the values
+	// and not involve other projects in semantics that they won't otherwise
+	// need to know.
+	addressableData []byte
+
+	ByteOrder binary.ByteOrder
+
+	// Name is the name of the IFD (the rightmost name in the path, sans any
+	// indices).
+	Name string
+
+	// IfdPath is a simple IFD path (e.g. IFD/GPSInfo). No indices.
+	IfdPath string
+
+	// FqIfdPath is a fully-qualified IFD path (e.g. IFD0/GPSInfo0). With
+	// indices.
+	FqIfdPath string
+
+	TagId uint16
+
+	Id int
+
+	ParentIfd *Ifd
+
+	// ParentTagIndex is our tag position in the parent IFD, if we had a parent
+	// (if `ParentIfd` is not nil and we weren't an IFD referenced as a sibling
+	// instead of as a child).
+	ParentTagIndex int
+
+	// Name   string
+	Index  int
+	Offset uint32
+
+	Entries        []*IfdTagEntry
+	EntriesByTagId map[uint16][]*IfdTagEntry
+
+	Children []*Ifd
+
+	ChildIfdIndex map[string]*Ifd
+
+	NextIfdOffset uint32
+	NextIfd       *Ifd
+
+	thumbnailData []byte
+
+	ifdMapping *IfdMapping
+	tagIndex   *TagIndex
+}
+
+func (ifd *Ifd) ChildWithIfdPath(ifdPath string) (childIfd *Ifd, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	for _, childIfd := range ifd.Children {
+		if childIfd.IfdPath == ifdPath {
+			return childIfd, nil
+		}
+	}
+
+	log.Panic(ErrTagNotFound)
+	return nil, nil
+}
+
+func (ifd *Ifd) TagValue(ite *IfdTagEntry) (value interface{}, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	value, err = ite.Value(ifd.addressableData, ifd.ByteOrder)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (ifd *Ifd) TagValueBytes(ite *IfdTagEntry) (value []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	value, err = ite.ValueBytes(ifd.addressableData, ifd.ByteOrder)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+// FindTagWithId returns a list of tags (usually just zero or one) that match
+// the given tag ID. This is efficient.
+func (ifd *Ifd) FindTagWithId(tagId uint16) (results []*IfdTagEntry, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	results, found := ifd.EntriesByTagId[tagId]
+	if found != true {
+		log.Panic(ErrTagNotFound)
+	}
+
+	return results, nil
+}
+
+// FindTagWithName returns a list of tags (usually just zero or one) that match
+// the given tag name. This is not efficient (though the labor is trivial).
+func (ifd *Ifd) FindTagWithName(tagName string) (results []*IfdTagEntry, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	it, err := ifd.tagIndex.GetWithName(ifd.IfdPath, tagName)
+	if log.Is(err, ErrTagNotFound) == true {
+		log.Panic(ErrTagNotStandard)
+	} else if err != nil {
+		log.Panic(err)
+	}
+
+	results = make([]*IfdTagEntry, 0)
+	for _, ite := range ifd.Entries {
+		if ite.TagId == it.Id {
+			results = append(results, ite)
+		}
+	}
+
+	if len(results) == 0 {
+		log.Panic(ErrTagNotFound)
+	}
+
+	return results, nil
+}
+
+func (ifd Ifd) String() string {
+	parentOffset := uint32(0)
+	if ifd.ParentIfd != nil {
+		parentOffset = ifd.ParentIfd.Offset
+	}
+
+	return fmt.Sprintf("Ifd<ID=(%d) IFD-PATH=[%s] INDEX=(%d) COUNT=(%d) OFF=(0x%04x) CHILDREN=(%d) PARENT=(0x%04x) NEXT-IFD=(0x%04x)>", ifd.Id, ifd.IfdPath, ifd.Index, len(ifd.Entries), ifd.Offset, len(ifd.Children), parentOffset, ifd.NextIfdOffset)
+}
+
+func (ifd *Ifd) Thumbnail() (data []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	if ifd.thumbnailData == nil {
+		log.Panic(ErrNoThumbnail)
+	}
+
+	return ifd.thumbnailData, nil
+}
+
+func (ifd *Ifd) dumpTags(tags []*IfdTagEntry) []*IfdTagEntry {
+	if tags == nil {
+		tags = make([]*IfdTagEntry, 0)
+	}
+
+	// Now, print the tags while also descending to child-IFDS as we encounter them.
+
+	ifdsFoundCount := 0
+
+	for _, tag := range ifd.Entries {
+		tags = append(tags, tag)
+
+		if tag.ChildIfdPath != "" {
+			ifdsFoundCount++
+
+			childIfd, found := ifd.ChildIfdIndex[tag.ChildIfdPath]
+			if found != true {
+				log.Panicf("alien child IFD referenced by a tag: [%s]", tag.ChildIfdPath)
+			}
+
+			tags = childIfd.dumpTags(tags)
+		}
+	}
+
+	if len(ifd.Children) != ifdsFoundCount {
+		log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.Children), ifdsFoundCount)
+	}
+
+	if ifd.NextIfd != nil {
+		tags = ifd.NextIfd.dumpTags(tags)
+	}
+
+	return tags
+}
+
+// DumpTags prints the IFD hierarchy.
+func (ifd *Ifd) DumpTags() []*IfdTagEntry {
+	return ifd.dumpTags(nil)
+}
+
+func (ifd *Ifd) printTagTree(populateValues bool, index, level int, nextLink bool) {
+	indent := strings.Repeat(" ", level*2)
+
+	prefix := " "
+	if nextLink {
+		prefix = ">"
+	}
+
+	fmt.Printf("%s%sIFD: %s\n", indent, prefix, ifd)
+
+	// Now, print the tags while also descending to child-IFDS as we encounter them.
+
+	ifdsFoundCount := 0
+
+	for _, tag := range ifd.Entries {
+		if tag.ChildIfdPath != "" {
+			fmt.Printf("%s - TAG: %s\n", indent, tag)
+		} else {
+			it, err := ifd.tagIndex.Get(ifd.IfdPath, tag.TagId)
+
+			tagName := ""
+			if err == nil {
+				tagName = it.Name
+			}
+
+			var value interface{}
+			if populateValues == true {
+				var err error
+
+				value, err = ifd.TagValue(tag)
+				if err != nil {
+					if err == ErrUnhandledUnknownTypedTag {
+						value = UnparseableUnknownTagValuePlaceholder
+					} else {
+						log.Panic(err)
+					}
+				}
+			}
+
+			fmt.Printf("%s - TAG: %s NAME=[%s] VALUE=[%v]\n", indent, tag, tagName, value)
+		}
+
+		if tag.ChildIfdPath != "" {
+			ifdsFoundCount++
+
+			childIfd, found := ifd.ChildIfdIndex[tag.ChildIfdPath]
+			if found != true {
+				log.Panicf("alien child IFD referenced by a tag: [%s]", tag.ChildIfdPath)
+			}
+
+			childIfd.printTagTree(populateValues, 0, level+1, false)
+		}
+	}
+
+	if len(ifd.Children) != ifdsFoundCount {
+		log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.Children), ifdsFoundCount)
+	}
+
+	if ifd.NextIfd != nil {
+		ifd.NextIfd.printTagTree(populateValues, index+1, level, true)
+	}
+}
+
+// PrintTagTree prints the IFD hierarchy.
+func (ifd *Ifd) PrintTagTree(populateValues bool) {
+	ifd.printTagTree(populateValues, 0, 0, false)
+}
+
+func (ifd *Ifd) printIfdTree(level int, nextLink bool) {
+	indent := strings.Repeat(" ", level*2)
+
+	prefix := " "
+	if nextLink {
+		prefix = ">"
+	}
+
+	fmt.Printf("%s%s%s\n", indent, prefix, ifd)
+
+	// Now, print the tags while also descending to child-IFDS as we encounter them.
+
+	ifdsFoundCount := 0
+
+	for _, tag := range ifd.Entries {
+		if tag.ChildIfdPath != "" {
+			ifdsFoundCount++
+
+			childIfd, found := ifd.ChildIfdIndex[tag.ChildIfdPath]
+			if found != true {
+				log.Panicf("alien child IFD referenced by a tag: [%s]", tag.ChildIfdPath)
+			}
+
+			childIfd.printIfdTree(level+1, false)
+		}
+	}
+
+	if len(ifd.Children) != ifdsFoundCount {
+		log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.Children), ifdsFoundCount)
+	}
+
+	if ifd.NextIfd != nil {
+		ifd.NextIfd.printIfdTree(level, true)
+	}
+}
+
+// PrintIfdTree prints the IFD hierarchy.
+func (ifd *Ifd) PrintIfdTree() {
+	ifd.printIfdTree(0, false)
+}
+
+func (ifd *Ifd) dumpTree(tagsDump []string, level int) []string {
+	if tagsDump == nil {
+		tagsDump = make([]string, 0)
+	}
+
+	indent := strings.Repeat(" ", level*2)
+
+	var ifdPhrase string
+	if ifd.ParentIfd != nil {
+		ifdPhrase = fmt.Sprintf("[%s]->[%s]:(%d)", ifd.ParentIfd.IfdPath, ifd.IfdPath, ifd.Index)
+	} else {
+		ifdPhrase = fmt.Sprintf("[ROOT]->[%s]:(%d)", ifd.IfdPath, ifd.Index)
+	}
+
+	startBlurb := fmt.Sprintf("%s> IFD %s TOP", indent, ifdPhrase)
+	tagsDump = append(tagsDump, startBlurb)
+
+	ifdsFoundCount := 0
+	for _, tag := range ifd.Entries {
+		tagsDump = append(tagsDump, fmt.Sprintf("%s  - (0x%04x)", indent, tag.TagId))
+
+		if tag.ChildIfdPath != "" {
+			ifdsFoundCount++
+
+			childIfd, found := ifd.ChildIfdIndex[tag.ChildIfdPath]
+			if found != true {
+				log.Panicf("alien child IFD referenced by a tag: [%s]", tag.ChildIfdPath)
+			}
+
+			tagsDump = childIfd.dumpTree(tagsDump, level+1)
+		}
+	}
+
+	if len(ifd.Children) != ifdsFoundCount {
+		log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.Children), ifdsFoundCount)
+	}
+
+	finishBlurb := fmt.Sprintf("%s< IFD %s BOTTOM", indent, ifdPhrase)
+	tagsDump = append(tagsDump, finishBlurb)
+
+	if ifd.NextIfd != nil {
+		siblingBlurb := fmt.Sprintf("%s* LINKING TO SIBLING IFD [%s]:(%d)", indent, ifd.NextIfd.IfdPath, ifd.NextIfd.Index)
+		tagsDump = append(tagsDump, siblingBlurb)
+
+		tagsDump = ifd.NextIfd.dumpTree(tagsDump, level)
+	}
+
+	return tagsDump
+}
+
+// DumpTree returns a list of strings describing the IFD hierarchy.
+func (ifd *Ifd) DumpTree() []string {
+	return ifd.dumpTree(nil, 0)
+}
+
+// GpsInfo parses and consolidates the GPS info. This can only be called on the
+// GPS IFD.
+func (ifd *Ifd) GpsInfo() (gi *GpsInfo, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// TODO(dustin): !! Also add functionality to update the GPS info.
+
+	gi = new(GpsInfo)
+
+	if ifd.IfdPath != IfdPathStandardGps {
+		log.Panicf("GPS can only be read on GPS IFD: [%s] != [%s]", ifd.IfdPath, IfdPathStandardGps)
+	}
+
+	if tags, found := ifd.EntriesByTagId[TagVersionId]; found == false {
+		// We've seen this. We'll just have to default to assuming we're in a
+		// 2.2.0.0 format.
+		ifdEnumerateLogger.Warningf(nil, "No GPS version tag (0x%04x) found.", TagVersionId)
+	} else {
+		hit := false
+		for _, acceptedGpsVersion := range ValidGpsVersions {
+			if bytes.Compare(tags[0].value, acceptedGpsVersion[:]) == 0 {
+				hit = true
+				break
+			}
+		}
+
+		if hit != true {
+			ifdEnumerateLogger.Warningf(nil, "GPS version not supported: %v", tags[0].value)
+			log.Panic(ErrNoGpsTags)
+		}
+	}
+
+	tags, found := ifd.EntriesByTagId[TagLatitudeId]
+	if found == false {
+		ifdEnumerateLogger.Warningf(nil, "latitude not found")
+		log.Panic(ErrNoGpsTags)
+	}
+
+	latitudeValue, err := ifd.TagValue(tags[0])
+	log.PanicIf(err)
+
+	// Look for whether North or South.
+	tags, found = ifd.EntriesByTagId[TagLatitudeRefId]
+	if found == false {
+		ifdEnumerateLogger.Warningf(nil, "latitude-ref not found")
+		log.Panic(ErrNoGpsTags)
+	}
+
+	latitudeRefValue, err := ifd.TagValue(tags[0])
+	log.PanicIf(err)
+
+	tags, found = ifd.EntriesByTagId[TagLongitudeId]
+	if found == false {
+		ifdEnumerateLogger.Warningf(nil, "longitude not found")
+		log.Panic(ErrNoGpsTags)
+	}
+
+	longitudeValue, err := ifd.TagValue(tags[0])
+	log.PanicIf(err)
+
+	// Look for whether West or East.
+	tags, found = ifd.EntriesByTagId[TagLongitudeRefId]
+	if found == false {
+		ifdEnumerateLogger.Warningf(nil, "longitude-ref not found")
+		log.Panic(ErrNoGpsTags)
+	}
+
+	longitudeRefValue, err := ifd.TagValue(tags[0])
+	log.PanicIf(err)
+
+	// Parse location.
+
+	latitudeRaw := latitudeValue.([]Rational)
+
+	gi.Latitude = GpsDegrees{
+		Orientation: latitudeRefValue.(string)[0],
+		Degrees:     float64(latitudeRaw[0].Numerator) / float64(latitudeRaw[0].Denominator),
+		Minutes:     float64(latitudeRaw[1].Numerator) / float64(latitudeRaw[1].Denominator),
+		Seconds:     float64(latitudeRaw[2].Numerator) / float64(latitudeRaw[2].Denominator),
+	}
+
+	longitudeRaw := longitudeValue.([]Rational)
+
+	gi.Longitude = GpsDegrees{
+		Orientation: longitudeRefValue.(string)[0],
+		Degrees:     float64(longitudeRaw[0].Numerator) / float64(longitudeRaw[0].Denominator),
+		Minutes:     float64(longitudeRaw[1].Numerator) / float64(longitudeRaw[1].Denominator),
+		Seconds:     float64(longitudeRaw[2].Numerator) / float64(longitudeRaw[2].Denominator),
+	}
+
+	// Parse altitude.
+
+	altitudeTags, foundAltitude := ifd.EntriesByTagId[TagAltitudeId]
+	altitudeRefTags, foundAltitudeRef := ifd.EntriesByTagId[TagAltitudeRefId]
+
+	if foundAltitude == true && foundAltitudeRef == true {
+		altitudeValue, err := ifd.TagValue(altitudeTags[0])
+		log.PanicIf(err)
+
+		altitudeRefValue, err := ifd.TagValue(altitudeRefTags[0])
+		log.PanicIf(err)
+
+		altitudeRaw := altitudeValue.([]Rational)
+		altitude := int(altitudeRaw[0].Numerator / altitudeRaw[0].Denominator)
+		if altitudeRefValue.([]byte)[0] == 1 {
+			altitude *= -1
+		}
+
+		gi.Altitude = altitude
+	}
+
+	// Parse time.
+
+	timestampTags, foundTimestamp := ifd.EntriesByTagId[TagTimestampId]
+	datestampTags, foundDatestamp := ifd.EntriesByTagId[TagDatestampId]
+
+	if foundTimestamp == true && foundDatestamp == true {
+		datestampValue, err := ifd.TagValue(datestampTags[0])
+		log.PanicIf(err)
+
+		dateParts := strings.Split(datestampValue.(string), ":")
+
+		year, err1 := strconv.ParseUint(dateParts[0], 10, 16)
+		month, err2 := strconv.ParseUint(dateParts[1], 10, 8)
+		day, err3 := strconv.ParseUint(dateParts[2], 10, 8)
+
+		if err1 == nil && err2 == nil && err3 == nil {
+			timestampValue, err := ifd.TagValue(timestampTags[0])
+			log.PanicIf(err)
+
+			timestampRaw := timestampValue.([]Rational)
+
+			hour := int(timestampRaw[0].Numerator / timestampRaw[0].Denominator)
+			minute := int(timestampRaw[1].Numerator / timestampRaw[1].Denominator)
+			second := int(timestampRaw[2].Numerator / timestampRaw[2].Denominator)
+
+			gi.Timestamp = time.Date(int(year), time.Month(month), int(day), hour, minute, second, 0, time.UTC)
+		}
+	}
+
+	return gi, nil
+}
+
+type ParsedTagVisitor func(*Ifd, *IfdTagEntry) error
+
+func (ifd *Ifd) EnumerateTagsRecursively(visitor ParsedTagVisitor) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	for ptr := ifd; ptr != nil; ptr = ptr.NextIfd {
+		for _, ite := range ifd.Entries {
+			if ite.ChildIfdPath != "" {
+				childIfd := ifd.ChildIfdIndex[ite.ChildIfdPath]
+
+				err := childIfd.EnumerateTagsRecursively(visitor)
+				log.PanicIf(err)
+			} else {
+				err := visitor(ifd, ite)
+				log.PanicIf(err)
+			}
+		}
+	}
+
+	return nil
+}
+
+func (ifd *Ifd) GetValueContext(ite *IfdTagEntry) *ValueContext {
+	return newValueContextFromTag(
+		ite,
+		ifd.addressableData,
+		ifd.ByteOrder)
+}
+
+type QueuedIfd struct {
+	Name      string
+	IfdPath   string
+	FqIfdPath string
+
+	TagId uint16
+
+	Index  int
+	Offset uint32
+	Parent *Ifd
+
+	// ParentTagIndex is our tag position in the parent IFD, if we had a parent
+	// (if `ParentIfd` is not nil and we weren't an IFD referenced as a sibling
+	// instead of as a child).
+	ParentTagIndex int
+}
+
+type IfdIndex struct {
+	RootIfd *Ifd
+	Ifds    []*Ifd
+	Tree    map[int]*Ifd
+	Lookup  map[string][]*Ifd
+}
+
+// Scan enumerates the different EXIF blocks (called IFDs).
+func (ie *IfdEnumerate) Collect(rootIfdOffset uint32, resolveValues bool) (index IfdIndex, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	tree := make(map[int]*Ifd)
+	ifds := make([]*Ifd, 0)
+	lookup := make(map[string][]*Ifd)
+
+	queue := []QueuedIfd{
+		{
+			Name:      IfdStandard,
+			IfdPath:   IfdStandard,
+			FqIfdPath: IfdStandard,
+
+			TagId: 0xffff,
+
+			Index:  0,
+			Offset: rootIfdOffset,
+		},
+	}
+
+	edges := make(map[uint32]*Ifd)
+
+	for {
+		if len(queue) == 0 {
+			break
+		}
+
+		qi := queue[0]
+
+		name := qi.Name
+		ifdPath := qi.IfdPath
+		fqIfdPath := qi.FqIfdPath
+
+		index := qi.Index
+		offset := qi.Offset
+		parentIfd := qi.Parent
+
+		queue = queue[1:]
+
+		ifdEnumerateLogger.Debugf(nil, "Parsing IFD [%s] (%d) at offset (%04x).", ifdPath, index, offset)
+		ite := ie.getTagEnumerator(offset)
+
+		nextIfdOffset, entries, thumbnailData, err := ie.ParseIfd(fqIfdPath, index, ite, nil, false, resolveValues)
+		log.PanicIf(err)
+
+		id := len(ifds)
+
+		entriesByTagId := make(map[uint16][]*IfdTagEntry)
+		for _, tag := range entries {
+			tags, found := entriesByTagId[tag.TagId]
+			if found == false {
+				tags = make([]*IfdTagEntry, 0)
+			}
+
+			entriesByTagId[tag.TagId] = append(tags, tag)
+		}
+
+		ifd := &Ifd{
+			addressableData: ie.exifData[ExifAddressableAreaStart:],
+
+			ByteOrder: ie.byteOrder,
+
+			Name:      name,
+			IfdPath:   ifdPath,
+			FqIfdPath: fqIfdPath,
+
+			TagId: qi.TagId,
+
+			Id: id,
+
+			ParentIfd:      parentIfd,
+			ParentTagIndex: qi.ParentTagIndex,
+
+			Index:          index,
+			Offset:         offset,
+			Entries:        entries,
+			EntriesByTagId: entriesByTagId,
+
+			// This is populated as each child is processed.
+			Children: make([]*Ifd, 0),
+
+			NextIfdOffset: nextIfdOffset,
+			thumbnailData: thumbnailData,
+
+			ifdMapping: ie.ifdMapping,
+			tagIndex:   ie.tagIndex,
+		}
+
+		// Add ourselves to a big list of IFDs.
+		ifds = append(ifds, ifd)
+
+		// Install ourselves into a by-id lookup table (keys are unique).
+		tree[id] = ifd
+
+		// Install into by-name buckets.
+
+		if list_, found := lookup[ifdPath]; found == true {
+			lookup[ifdPath] = append(list_, ifd)
+		} else {
+			list_ = make([]*Ifd, 1)
+			list_[0] = ifd
+
+			lookup[ifdPath] = list_
+		}
+
+		// Add a link from the previous IFD in the chain to us.
+		if previousIfd, found := edges[offset]; found == true {
+			previousIfd.NextIfd = ifd
+		}
+
+		// Attach as a child to our parent (where we appeared as a tag in
+		// that IFD).
+		if parentIfd != nil {
+			parentIfd.Children = append(parentIfd.Children, ifd)
+		}
+
+		// Determine if any of our entries is a child IFD and queue it.
+		for i, entry := range entries {
+			if entry.ChildIfdPath == "" {
+				continue
+			}
+
+			qi := QueuedIfd{
+				Name:      entry.ChildIfdName,
+				IfdPath:   entry.ChildIfdPath,
+				FqIfdPath: entry.ChildFqIfdPath,
+				TagId:     entry.TagId,
+
+				Index:          0,
+				Offset:         entry.ValueOffset,
+				Parent:         ifd,
+				ParentTagIndex: i,
+			}
+
+			queue = append(queue, qi)
+		}
+
+		// If there's another IFD in the chain.
+		if nextIfdOffset != 0 {
+			// Allow the next link to know what the previous link was.
+			edges[nextIfdOffset] = ifd
+
+			siblingIndex := index + 1
+
+			var fqIfdPath string
+			if parentIfd != nil {
+				fqIfdPath = fmt.Sprintf("%s/%s%d", parentIfd.FqIfdPath, name, siblingIndex)
+			} else {
+				fqIfdPath = fmt.Sprintf("%s%d", name, siblingIndex)
+			}
+
+			qi := QueuedIfd{
+				Name:      name,
+				IfdPath:   ifdPath,
+				FqIfdPath: fqIfdPath,
+				TagId:     0xffff,
+				Index:     siblingIndex,
+				Offset:    nextIfdOffset,
+			}
+
+			queue = append(queue, qi)
+		}
+	}
+
+	index.RootIfd = tree[0]
+	index.Ifds = ifds
+	index.Tree = tree
+	index.Lookup = lookup
+
+	err = ie.setChildrenIndex(index.RootIfd)
+	log.PanicIf(err)
+
+	return index, nil
+}
+
+func (ie *IfdEnumerate) setChildrenIndex(ifd *Ifd) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	childIfdIndex := make(map[string]*Ifd)
+	for _, childIfd := range ifd.Children {
+		childIfdIndex[childIfd.IfdPath] = childIfd
+	}
+
+	ifd.ChildIfdIndex = childIfdIndex
+
+	for _, childIfd := range ifd.Children {
+		err := ie.setChildrenIndex(childIfd)
+		log.PanicIf(err)
+	}
+
+	return nil
+}
+
+// ParseOneIfd is a hack to use an IE to parse a raw IFD block. Can be used for
+// testing.
+func ParseOneIfd(ifdMapping *IfdMapping, tagIndex *TagIndex, fqIfdPath, ifdPath string, byteOrder binary.ByteOrder, ifdBlock []byte, visitor RawTagVisitor, resolveValues bool) (nextIfdOffset uint32, entries []*IfdTagEntry, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	ie := NewIfdEnumerate(ifdMapping, tagIndex, make([]byte, 0), byteOrder)
+	ite := NewIfdTagEnumerator(ifdBlock, byteOrder, 0)
+
+	nextIfdOffset, entries, _, err = ie.ParseIfd(fqIfdPath, 0, ite, visitor, true, resolveValues)
+	log.PanicIf(err)
+
+	return nextIfdOffset, entries, nil
+}
+
+// ParseOneTag is a hack to use an IE to parse a raw tag block.
+func ParseOneTag(ifdMapping *IfdMapping, tagIndex *TagIndex, fqIfdPath, ifdPath string, byteOrder binary.ByteOrder, tagBlock []byte, resolveValue bool) (tag *IfdTagEntry, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	ie := NewIfdEnumerate(ifdMapping, tagIndex, make([]byte, 0), byteOrder)
+	ite := NewIfdTagEnumerator(tagBlock, byteOrder, 0)
+
+	tag, err = ie.parseTag(fqIfdPath, 0, ite, resolveValue)
+	log.PanicIf(err)
+
+	return tag, nil
+}
+
+func FindIfdFromRootIfd(rootIfd *Ifd, ifdPath string) (ifd *Ifd, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// TODO(dustin): !! Add test.
+
+	lineage, err := rootIfd.ifdMapping.ResolvePath(ifdPath)
+	log.PanicIf(err)
+
+	// Confirm the first IFD is our root IFD type, and then prune it because
+	// from then on we'll be searching down through our children.
+
+	if len(lineage) == 0 {
+		log.Panicf("IFD path must be non-empty.")
+	} else if lineage[0].Name != IfdStandard {
+		log.Panicf("First IFD path item must be [%s].", IfdStandard)
+	}
+
+	desiredRootIndex := lineage[0].Index
+	lineage = lineage[1:]
+
+	// TODO(dustin): !! This is a poorly conceived fix that just doubles the work we already have to do below, which then interacts badly with the indices not being properly represented in the IFD-phrase.
+	// TODO(dustin): !! <-- However, we're not sure whether we shouldn't store a secondary IFD-path with the indices. Some IFDs may not necessarily restrict which IFD indices they can be a child of (only the IFD itself matters). Validation should be delegated to the caller.
+	thisIfd := rootIfd
+	for currentRootIndex := 0; currentRootIndex < desiredRootIndex; currentRootIndex++ {
+		if thisIfd.NextIfd == nil {
+			log.Panicf("Root-IFD index (%d) does not exist in the data.", currentRootIndex)
+		}
+
+		thisIfd = thisIfd.NextIfd
+	}
+
+	for i, itii := range lineage {
+		var hit *Ifd
+		for _, childIfd := range thisIfd.Children {
+			if childIfd.TagId == itii.TagId {
+				hit = childIfd
+				break
+			}
+		}
+
+		// If we didn't find the child, add it.
+		if hit == nil {
+			log.Panicf("IFD [%s] in [%s] not found: %s", itii.Name, ifdPath, thisIfd.Children)
+		}
+
+		thisIfd = hit
+
+		// If we didn't find the sibling, add it.
+		for i = 0; i < itii.Index; i++ {
+			if thisIfd.NextIfd == nil {
+				log.Panicf("IFD [%s] does not have (%d) occurrences/siblings\n", thisIfd.IfdPath, itii.Index)
+			}
+
+			thisIfd = thisIfd.NextIfd
+		}
+	}
+
+	return thisIfd, nil
+}
diff --git a/v2/ifd_enumerate_test.go b/v2/ifd_enumerate_test.go
new file mode 100644
index 0000000..d8e81fd
--- /dev/null
+++ b/v2/ifd_enumerate_test.go
@@ -0,0 +1,532 @@
+package exif
+
+import (
+	"bytes"
+	"fmt"
+	"path"
+	"reflect"
+	"testing"
+
+	"encoding/binary"
+	"io/ioutil"
+
+	"github.com/dsoprea/go-logging"
+)
+
+func TestIfdTagEntry_ValueBytes(t *testing.T) {
+	byteOrder := binary.BigEndian
+	ve := NewValueEncoder(byteOrder)
+
+	original := []byte("original text")
+
+	ed, err := ve.encodeBytes(original)
+	log.PanicIf(err)
+
+	// Now, pass the raw encoded value as if it was the entire addressable area
+	// and provide an offset of 0 as if it was a real block of data and this
+	// value happened to be recorded at the beginning.
+
+	ite := IfdTagEntry{
+		TagType:     TypeByte,
+		UnitCount:   uint32(len(original)),
+		ValueOffset: 0,
+	}
+
+	decodedBytes, err := ite.ValueBytes(ed.Encoded, byteOrder)
+	log.PanicIf(err)
+
+	if bytes.Compare(decodedBytes, original) != 0 {
+		t.Fatalf("Bytes not decoded correctly.")
+	}
+}
+
+func TestIfdTagEntry_ValueBytes_RealData(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Test failure.")
+		}
+	}()
+
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	eh, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	var ite *IfdTagEntry
+	for _, thisIte := range index.RootIfd.Entries {
+		if thisIte.TagId == 0x0110 {
+			ite = thisIte
+			break
+		}
+	}
+
+	if ite == nil {
+		t.Fatalf("Tag not found.")
+	}
+
+	addressableData := rawExif[ExifAddressableAreaStart:]
+	decodedBytes, err := ite.ValueBytes(addressableData, eh.ByteOrder)
+	log.PanicIf(err)
+
+	expected := []byte("Canon EOS 5D Mark III")
+	expected = append(expected, 0)
+
+	if len(decodedBytes) != int(ite.UnitCount) {
+		t.Fatalf("Decoded bytes not the right count.")
+	} else if bytes.Compare(decodedBytes, expected) != 0 {
+		t.Fatalf("Decoded bytes not correct.")
+	}
+}
+
+func TestIfd_FindTagWithId_Hit(t *testing.T) {
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	ifd := index.RootIfd
+	results, err := ifd.FindTagWithId(0x011b)
+
+	if len(results) != 1 {
+		t.Fatalf("Exactly one result was not found: (%d)", len(results))
+	} else if results[0].TagId != 0x011b {
+		t.Fatalf("The result was not expected: %v", results[0])
+	}
+}
+
+func TestIfd_FindTagWithId_Miss(t *testing.T) {
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	ifd := index.RootIfd
+
+	_, err = ifd.FindTagWithId(0xffff)
+	if err == nil {
+		t.Fatalf("Expected error for not-found tag.")
+	} else if log.Is(err, ErrTagNotFound) == false {
+		log.Panic(err)
+	}
+}
+
+func TestIfd_FindTagWithName_Hit(t *testing.T) {
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	ifd := index.RootIfd
+	results, err := ifd.FindTagWithName("YResolution")
+
+	if len(results) != 1 {
+		t.Fatalf("Exactly one result was not found: (%d)", len(results))
+	} else if results[0].TagId != 0x011b {
+		t.Fatalf("The result was not expected: %v", results[0])
+	}
+}
+
+func TestIfd_FindTagWithName_Miss(t *testing.T) {
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	ifd := index.RootIfd
+
+	_, err = ifd.FindTagWithName("PlanarConfiguration")
+	if err == nil {
+		t.Fatalf("Expected error for not-found tag.")
+	} else if log.Is(err, ErrTagNotFound) == false {
+		log.Panic(err)
+	}
+}
+
+func TestIfd_FindTagWithName_NonStandard(t *testing.T) {
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	ifd := index.RootIfd
+
+	_, err = ifd.FindTagWithName("GeorgeNotAtHome")
+	if err == nil {
+		t.Fatalf("Expected error for not-found tag.")
+	} else if log.Is(err, ErrTagNotStandard) == false {
+		log.Panic(err)
+	}
+}
+
+func TestIfd_Thumbnail(t *testing.T) {
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	ifd := index.RootIfd
+
+	if ifd.NextIfd == nil {
+		t.Fatalf("There is no IFD1.")
+	}
+
+	// The thumbnail is in IFD1 (The second root IFD).
+	actual, err := ifd.NextIfd.Thumbnail()
+	log.PanicIf(err)
+
+	expectedFilepath := path.Join(assetsPath, "NDM_8901.jpg.thumbnail")
+
+	expected, err := ioutil.ReadFile(expectedFilepath)
+	log.PanicIf(err)
+
+	if bytes.Compare(actual, expected) != 0 {
+		t.Fatalf("thumbnail not correct")
+	}
+}
+
+func TestIfd_GpsInfo(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Test failure.")
+		}
+	}()
+
+	filepath := path.Join(assetsPath, "gps.jpg")
+
+	rawExif, err := SearchFileAndExtractExif(filepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	ifd, err := index.RootIfd.ChildWithIfdPath(IfdPathStandardGps)
+	log.PanicIf(err)
+
+	gi, err := ifd.GpsInfo()
+	log.PanicIf(err)
+
+	if gi.Latitude.Orientation != 'N' || gi.Latitude.Degrees != 26 || gi.Latitude.Minutes != 35 || gi.Latitude.Seconds != 12 {
+		t.Fatalf("latitude not correct")
+	} else if gi.Longitude.Orientation != 'W' || gi.Longitude.Degrees != 80 || gi.Longitude.Minutes != 3 || gi.Longitude.Seconds != 13 {
+		t.Fatalf("longitude not correct")
+	} else if gi.Altitude != 0 {
+		t.Fatalf("altitude not correct")
+	} else if gi.Timestamp.Unix() != 1524964977 {
+		t.Fatalf("timestamp not correct")
+	} else if gi.Altitude != 0 {
+		t.Fatalf("altitude not correct")
+	}
+}
+
+func TestIfd_EnumerateTagsRecursively(t *testing.T) {
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	collected := make([][2]interface{}, 0)
+
+	cb := func(ifd *Ifd, ite *IfdTagEntry) error {
+		item := [2]interface{}{
+			ifd.IfdPath,
+			int(ite.TagId),
+		}
+
+		collected = append(collected, item)
+
+		return nil
+	}
+
+	err = index.RootIfd.EnumerateTagsRecursively(cb)
+	log.PanicIf(err)
+
+	expected := [][2]interface{}{
+		[2]interface{}{"IFD", 0x010f},
+		[2]interface{}{"IFD", 0x0110},
+		[2]interface{}{"IFD", 0x0112},
+		[2]interface{}{"IFD", 0x011a},
+		[2]interface{}{"IFD", 0x011b},
+		[2]interface{}{"IFD", 0x0128},
+		[2]interface{}{"IFD", 0x0132},
+		[2]interface{}{"IFD", 0x013b},
+		[2]interface{}{"IFD", 0x0213},
+		[2]interface{}{"IFD", 0x8298},
+		[2]interface{}{"IFD/Exif", 0x829a},
+		[2]interface{}{"IFD/Exif", 0x829d},
+		[2]interface{}{"IFD/Exif", 0x8822},
+		[2]interface{}{"IFD/Exif", 0x8827},
+		[2]interface{}{"IFD/Exif", 0x8830},
+		[2]interface{}{"IFD/Exif", 0x8832},
+		[2]interface{}{"IFD/Exif", 0x9000},
+		[2]interface{}{"IFD/Exif", 0x9003},
+		[2]interface{}{"IFD/Exif", 0x9004},
+		[2]interface{}{"IFD/Exif", 0x9101},
+		[2]interface{}{"IFD/Exif", 0x9201},
+		[2]interface{}{"IFD/Exif", 0x9202},
+		[2]interface{}{"IFD/Exif", 0x9204},
+		[2]interface{}{"IFD/Exif", 0x9207},
+		[2]interface{}{"IFD/Exif", 0x9209},
+		[2]interface{}{"IFD/Exif", 0x920a},
+		[2]interface{}{"IFD/Exif", 0x927c},
+		[2]interface{}{"IFD/Exif", 0x9286},
+		[2]interface{}{"IFD/Exif", 0x9290},
+		[2]interface{}{"IFD/Exif", 0x9291},
+		[2]interface{}{"IFD/Exif", 0x9292},
+		[2]interface{}{"IFD/Exif", 0xa000},
+		[2]interface{}{"IFD/Exif", 0xa001},
+		[2]interface{}{"IFD/Exif", 0xa002},
+		[2]interface{}{"IFD/Exif", 0xa003},
+		[2]interface{}{"IFD/Exif/Iop", 0x0001},
+		[2]interface{}{"IFD/Exif/Iop", 0x0002},
+		[2]interface{}{"IFD/Exif", 0xa20e},
+		[2]interface{}{"IFD/Exif", 0xa20f},
+		[2]interface{}{"IFD/Exif", 0xa210},
+		[2]interface{}{"IFD/Exif", 0xa401},
+		[2]interface{}{"IFD/Exif", 0xa402},
+		[2]interface{}{"IFD/Exif", 0xa403},
+		[2]interface{}{"IFD/Exif", 0xa406},
+		[2]interface{}{"IFD/Exif", 0xa430},
+		[2]interface{}{"IFD/Exif", 0xa431},
+		[2]interface{}{"IFD/Exif", 0xa432},
+		[2]interface{}{"IFD/Exif", 0xa434},
+		[2]interface{}{"IFD/Exif", 0xa435},
+		[2]interface{}{"IFD/GPSInfo", 0x0000},
+		[2]interface{}{"IFD", 0x010f},
+		[2]interface{}{"IFD", 0x0110},
+		[2]interface{}{"IFD", 0x0112},
+		[2]interface{}{"IFD", 0x011a},
+		[2]interface{}{"IFD", 0x011b},
+		[2]interface{}{"IFD", 0x0128},
+		[2]interface{}{"IFD", 0x0132},
+		[2]interface{}{"IFD", 0x013b},
+		[2]interface{}{"IFD", 0x0213},
+		[2]interface{}{"IFD", 0x8298},
+		[2]interface{}{"IFD/Exif", 0x829a},
+		[2]interface{}{"IFD/Exif", 0x829d},
+		[2]interface{}{"IFD/Exif", 0x8822},
+		[2]interface{}{"IFD/Exif", 0x8827},
+		[2]interface{}{"IFD/Exif", 0x8830},
+		[2]interface{}{"IFD/Exif", 0x8832},
+		[2]interface{}{"IFD/Exif", 0x9000},
+		[2]interface{}{"IFD/Exif", 0x9003},
+		[2]interface{}{"IFD/Exif", 0x9004},
+		[2]interface{}{"IFD/Exif", 0x9101},
+		[2]interface{}{"IFD/Exif", 0x9201},
+		[2]interface{}{"IFD/Exif", 0x9202},
+		[2]interface{}{"IFD/Exif", 0x9204},
+		[2]interface{}{"IFD/Exif", 0x9207},
+		[2]interface{}{"IFD/Exif", 0x9209},
+		[2]interface{}{"IFD/Exif", 0x920a},
+		[2]interface{}{"IFD/Exif", 0x927c},
+		[2]interface{}{"IFD/Exif", 0x9286},
+		[2]interface{}{"IFD/Exif", 0x9290},
+		[2]interface{}{"IFD/Exif", 0x9291},
+		[2]interface{}{"IFD/Exif", 0x9292},
+		[2]interface{}{"IFD/Exif", 0xa000},
+		[2]interface{}{"IFD/Exif", 0xa001},
+		[2]interface{}{"IFD/Exif", 0xa002},
+		[2]interface{}{"IFD/Exif", 0xa003},
+		[2]interface{}{"IFD/Exif/Iop", 0x0001},
+		[2]interface{}{"IFD/Exif/Iop", 0x0002},
+		[2]interface{}{"IFD/Exif", 0xa20e},
+		[2]interface{}{"IFD/Exif", 0xa20f},
+		[2]interface{}{"IFD/Exif", 0xa210},
+		[2]interface{}{"IFD/Exif", 0xa401},
+		[2]interface{}{"IFD/Exif", 0xa402},
+		[2]interface{}{"IFD/Exif", 0xa403},
+		[2]interface{}{"IFD/Exif", 0xa406},
+		[2]interface{}{"IFD/Exif", 0xa430},
+		[2]interface{}{"IFD/Exif", 0xa431},
+		[2]interface{}{"IFD/Exif", 0xa432},
+		[2]interface{}{"IFD/Exif", 0xa434},
+		[2]interface{}{"IFD/Exif", 0xa435},
+		[2]interface{}{"IFD/GPSInfo", 0x0000},
+	}
+
+	if reflect.DeepEqual(collected, expected) != true {
+		fmt.Printf("ACTUAL:\n")
+		fmt.Printf("\n")
+
+		for _, item := range collected {
+			fmt.Printf("[2]interface{} { \"%s\", 0x%04x },\n", item[0], item[1])
+		}
+
+		fmt.Printf("\n")
+
+		fmt.Printf("EXPECTED:\n")
+		fmt.Printf("\n")
+
+		for _, item := range expected {
+			fmt.Printf("[2]interface{} { \"%s\", 0x%04x },\n", item[0], item[1])
+		}
+
+		fmt.Printf("\n")
+
+		t.Fatalf("tags not visited correctly")
+	}
+}
+
+func ExampleIfd_EnumerateTagsRecursively() {
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	cb := func(ifd *Ifd, ite *IfdTagEntry) error {
+
+		// Something useful.
+
+		return nil
+	}
+
+	err = index.RootIfd.EnumerateTagsRecursively(cb)
+	log.PanicIf(err)
+
+	// Output:
+}
+
+func ExampleIfd_GpsInfo() {
+	filepath := path.Join(assetsPath, "gps.jpg")
+
+	rawExif, err := SearchFileAndExtractExif(filepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	ifd, err := index.RootIfd.ChildWithIfdPath(IfdPathStandardGps)
+	log.PanicIf(err)
+
+	gi, err := ifd.GpsInfo()
+	log.PanicIf(err)
+
+	fmt.Printf("%s\n", gi)
+
+	// Output:
+	// GpsInfo<LAT=(26.58667) LON=(-80.05361) ALT=(0) TIME=[2018-04-29 01:22:57 +0000 UTC]>
+}
+
+func ExampleIfd_FindTagWithName() {
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	tagName := "Model"
+
+	// We know the tag we want is on IFD0 (the first/root IFD).
+	results, err := index.RootIfd.FindTagWithName(tagName)
+	log.PanicIf(err)
+
+	// This should never happen.
+	if len(results) != 1 {
+		log.Panicf("there wasn't exactly one result")
+	}
+
+	ite := results[0]
+
+	valueRaw, err := index.RootIfd.TagValue(ite)
+	log.PanicIf(err)
+
+	value := valueRaw.(string)
+	fmt.Println(value)
+
+	// Output:
+	// Canon EOS 5D Mark III
+}
diff --git a/v2/ifd_tag_entry.go b/v2/ifd_tag_entry.go
new file mode 100644
index 0000000..59e79cc
--- /dev/null
+++ b/v2/ifd_tag_entry.go
@@ -0,0 +1,233 @@
+package exif
+
+import (
+	"fmt"
+	"reflect"
+
+	"encoding/binary"
+
+	"github.com/dsoprea/go-logging"
+)
+
+var (
+	iteLogger = log.NewLogger("exif.ifd_tag_entry")
+)
+
+type IfdTagEntry struct {
+	TagId          uint16
+	TagIndex       int
+	TagType        TagTypePrimitive
+	UnitCount      uint32
+	ValueOffset    uint32
+	RawValueOffset []byte
+
+	// ChildIfdName is the right most atom in the IFD-path. We need this to
+	// construct the fully-qualified IFD-path.
+	ChildIfdName string
+
+	// ChildIfdPath is the IFD-path of the child if this tag represents a child
+	// IFD.
+	ChildIfdPath string
+
+	// ChildFqIfdPath is the IFD-path of the child if this tag represents a
+	// child IFD. Includes indices.
+	ChildFqIfdPath string
+
+	// TODO(dustin): !! IB's host the child-IBs directly in the tag, but that's not the case here. Refactor to accomodate it for a consistent experience.
+
+	// IfdPath is the IFD that this tag belongs to.
+	IfdPath string
+
+	// TODO(dustin): !! We now parse and read the value immediately. Update the rest of the logic to use this and get rid of all of the staggered and different resolution mechanisms.
+	value              []byte
+	isUnhandledUnknown bool
+}
+
+func (ite *IfdTagEntry) String() string {
+	return fmt.Sprintf("IfdTagEntry<TAG-IFD-PATH=[%s] TAG-ID=(0x%04x) TAG-TYPE=[%s] UNIT-COUNT=(%d)>", ite.IfdPath, ite.TagId, TypeNames[ite.TagType], ite.UnitCount)
+}
+
+// TODO(dustin): TODO(dustin): Stop exporting IfdPath and TagId.
+//
+// func (ite *IfdTagEntry) IfdPath() string {
+// 	return ite.IfdPath
+// }
+
+// TODO(dustin): TODO(dustin): Stop exporting IfdPath and TagId.
+//
+// func (ite *IfdTagEntry) TagId() uint16 {
+// 	return ite.TagId
+// }
+
+// ValueString renders a string from whatever the value in this tag is.
+func (ite *IfdTagEntry) ValueString(addressableData []byte, byteOrder binary.ByteOrder) (value string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	valueContext :=
+		newValueContextFromTag(
+			ite,
+			addressableData,
+			byteOrder)
+
+	if ite.TagType == TypeUndefined {
+		valueRaw, err := valueContext.Undefined()
+		log.PanicIf(err)
+
+		value = fmt.Sprintf("%v", valueRaw)
+	} else {
+		value, err = valueContext.Format()
+		log.PanicIf(err)
+	}
+
+	return value, nil
+}
+
+// ValueBytes renders a specific list of bytes from the value in this tag.
+func (ite *IfdTagEntry) ValueBytes(addressableData []byte, byteOrder binary.ByteOrder) (value []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// Return the exact bytes of the unknown-type value. Returning a string
+	// (`ValueString`) is easy because we can just pass everything to
+	// `Sprintf()`. Returning the raw, typed value (`Value`) is easy
+	// (obviously). However, here, in order to produce the list of bytes, we
+	// need to coerce whatever `Undefined()` returns.
+	if ite.TagType == TypeUndefined {
+		valueContext :=
+			newValueContextFromTag(
+				ite,
+				addressableData,
+				byteOrder)
+
+		value, err := valueContext.Undefined()
+		log.PanicIf(err)
+
+		switch value.(type) {
+		case []byte:
+			return value.([]byte), nil
+		case TagUnknownType_UnknownValue:
+			b := []byte(value.(TagUnknownType_UnknownValue))
+			return b, nil
+		case string:
+			return []byte(value.(string)), nil
+		case UnknownTagValue:
+			valueBytes, err := value.(UnknownTagValue).ValueBytes()
+			log.PanicIf(err)
+
+			return valueBytes, nil
+		default:
+			// TODO(dustin): !! Finish translating the rest of the types (make reusable and replace into other similar implementations?)
+			log.Panicf("can not produce bytes for unknown-type tag (0x%04x) (2): [%s]", ite.TagId, reflect.TypeOf(value))
+		}
+	}
+
+	originalType := NewTagType(ite.TagType, byteOrder)
+	byteCount := uint32(originalType.Type().Size()) * ite.UnitCount
+
+	tt := NewTagType(TypeByte, byteOrder)
+
+	if tt.valueIsEmbedded(byteCount) == true {
+		iteLogger.Debugf(nil, "Reading BYTE value (ITE; embedded).")
+
+		// In this case, the bytes normally used for the offset are actually
+		// data.
+		value, err = tt.ParseBytes(ite.RawValueOffset, byteCount)
+		log.PanicIf(err)
+	} else {
+		iteLogger.Debugf(nil, "Reading BYTE value (ITE; at offset).")
+
+		value, err = tt.ParseBytes(addressableData[ite.ValueOffset:], byteCount)
+		log.PanicIf(err)
+	}
+
+	return value, nil
+}
+
+// Value returns the specific, parsed, typed value from the tag.
+func (ite *IfdTagEntry) Value(addressableData []byte, byteOrder binary.ByteOrder) (value interface{}, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	valueContext :=
+		newValueContextFromTag(
+			ite,
+			addressableData,
+			byteOrder)
+
+	if ite.TagType == TypeUndefined {
+		value, err = valueContext.Undefined()
+		log.PanicIf(err)
+	} else {
+		tt := NewTagType(ite.TagType, byteOrder)
+
+		value, err = tt.Resolve(valueContext)
+		log.PanicIf(err)
+	}
+
+	return value, nil
+}
+
+// IfdTagEntryValueResolver instances know how to resolve the values for any
+// tag for a particular EXIF block.
+type IfdTagEntryValueResolver struct {
+	addressableData []byte
+	byteOrder       binary.ByteOrder
+}
+
+func NewIfdTagEntryValueResolver(exifData []byte, byteOrder binary.ByteOrder) (itevr *IfdTagEntryValueResolver) {
+	return &IfdTagEntryValueResolver{
+		addressableData: exifData[ExifAddressableAreaStart:],
+		byteOrder:       byteOrder,
+	}
+}
+
+// ValueBytes will resolve embedded or allocated data from the tag and return the raw bytes.
+func (itevr *IfdTagEntryValueResolver) ValueBytes(ite *IfdTagEntry) (value []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// OBSOLETE(dustin): This is now redundant. Use `(*ValueContext).readRawEncoded()` instead of this method.
+
+	valueContext := newValueContextFromTag(
+		ite,
+		itevr.addressableData,
+		itevr.byteOrder)
+
+	rawBytes, err := valueContext.readRawEncoded()
+	log.PanicIf(err)
+
+	return rawBytes, nil
+}
+
+func (itevr *IfdTagEntryValueResolver) Value(ite *IfdTagEntry) (value interface{}, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// OBSOLETE(dustin): This is now redundant. Use `(*ValueContext).Values()` instead of this method.
+
+	valueContext := newValueContextFromTag(
+		ite,
+		itevr.addressableData,
+		itevr.byteOrder)
+
+	values, err := valueContext.Values()
+	log.PanicIf(err)
+
+	return values, nil
+}
diff --git a/v2/ifd_tag_entry_test.go b/v2/ifd_tag_entry_test.go
new file mode 100644
index 0000000..8aa1db0
--- /dev/null
+++ b/v2/ifd_tag_entry_test.go
@@ -0,0 +1,210 @@
+package exif
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/dsoprea/go-logging"
+)
+
+func TestIfdTagEntry_ValueString_Allocated(t *testing.T) {
+	ite := IfdTagEntry{
+		TagId:          0x1,
+		TagIndex:       0,
+		TagType:        TypeByte,
+		UnitCount:      6,
+		ValueOffset:    0x0,
+		RawValueOffset: []byte{0x0, 0x0, 0x0, 0x0},
+		IfdPath:        IfdPathStandard,
+	}
+
+	data := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
+
+	value, err := ite.ValueString(data, TestDefaultByteOrder)
+	log.PanicIf(err)
+
+	expected := "11 22 33 44 55 66"
+	if value != expected {
+		t.Fatalf("Value not expected: [%s] != [%s]", value, expected)
+	}
+}
+
+func TestIfdTagEntry_ValueString_Embedded(t *testing.T) {
+	data := []byte{0x11, 0x22, 0x33, 0x44}
+
+	ite := IfdTagEntry{
+		TagId:          0x1,
+		TagIndex:       0,
+		TagType:        TypeByte,
+		UnitCount:      4,
+		ValueOffset:    0,
+		RawValueOffset: data,
+		IfdPath:        IfdPathStandard,
+	}
+
+	value, err := ite.ValueString(nil, TestDefaultByteOrder)
+	log.PanicIf(err)
+
+	expected := "11 22 33 44"
+	if value != expected {
+		t.Fatalf("Value not expected: [%s] != [%s]", value, expected)
+	}
+}
+
+func TestIfdTagEntry_ValueString_Unknown(t *testing.T) {
+	data := []uint8{'0', '2', '3', '0'}
+
+	ite := IfdTagEntry{
+		TagId:          0x9000,
+		TagIndex:       0,
+		TagType:        TypeUndefined,
+		UnitCount:      4,
+		ValueOffset:    0x0,
+		RawValueOffset: data,
+		IfdPath:        IfdPathStandardExif,
+	}
+
+	value, err := ite.ValueString(nil, TestDefaultByteOrder)
+	log.PanicIf(err)
+
+	expected := "0230"
+	if value != expected {
+		t.Fatalf("Value not expected: [%s] != [%s]", value, expected)
+	}
+}
+
+func TestIfdTagEntry_ValueBytes_Allocated(t *testing.T) {
+	ite := IfdTagEntry{
+		TagId:          0x1,
+		TagIndex:       0,
+		TagType:        TypeByte,
+		UnitCount:      6,
+		ValueOffset:    0x0,
+		RawValueOffset: []byte{0x0, 0x0, 0x0, 0x0},
+		IfdPath:        IfdPathStandard,
+	}
+
+	data := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
+
+	value, err := ite.ValueBytes(data, TestDefaultByteOrder)
+	log.PanicIf(err)
+
+	if bytes.Compare(value, data) != 0 {
+		t.Fatalf("Value not expected: [%s] != [%s]", value, data)
+	}
+}
+
+func TestIfdTagEntry_ValueBytes_Embedded(t *testing.T) {
+	data := []byte{0x11, 0x22, 0x33, 0x44}
+
+	ite := IfdTagEntry{
+		TagId:          0x1,
+		TagIndex:       0,
+		TagType:        TypeByte,
+		UnitCount:      4,
+		ValueOffset:    0x0,
+		RawValueOffset: data,
+		IfdPath:        IfdPathStandard,
+	}
+
+	value, err := ite.ValueBytes(nil, TestDefaultByteOrder)
+	log.PanicIf(err)
+
+	if bytes.Compare(value, data) != 0 {
+		t.Fatalf("Value not expected: [%s] != [%s]", value, data)
+	}
+}
+
+func TestIfdTagEntry_Value_Normal(t *testing.T) {
+	data := []byte{0x11, 0x22, 0x33, 0x44}
+
+	ite := IfdTagEntry{
+		TagId:          0x1,
+		TagIndex:       0,
+		TagType:        TypeByte,
+		UnitCount:      4,
+		ValueOffset:    0x0,
+		RawValueOffset: data,
+		IfdPath:        IfdPathStandard,
+	}
+
+	value, err := ite.Value(nil, TestDefaultByteOrder)
+	log.PanicIf(err)
+
+	if bytes.Compare(value.([]byte), data) != 0 {
+		t.Fatalf("Value not expected: [%s] != [%s]", value, data)
+	}
+}
+
+func TestIfdTagEntry_Value_Unknown(t *testing.T) {
+	data := []uint8{'0', '2', '3', '0'}
+
+	ite := IfdTagEntry{
+		TagId:          0x9000,
+		TagIndex:       0,
+		TagType:        TypeUndefined,
+		UnitCount:      4,
+		ValueOffset:    0x0,
+		RawValueOffset: data,
+		IfdPath:        IfdPathStandardExif,
+	}
+
+	value, err := ite.Value(nil, TestDefaultByteOrder)
+	log.PanicIf(err)
+
+	gs := value.(TagUnknownType_GeneralString)
+
+	vb, err := gs.ValueBytes()
+	log.PanicIf(err)
+
+	if bytes.Compare(vb, data) != 0 {
+		t.Fatalf("Value not expected: [%s] != [%s]", value, data)
+	}
+}
+
+func TestIfdTagEntry_String(t *testing.T) {
+	ite := IfdTagEntry{
+		TagId:          0x1,
+		TagIndex:       0,
+		TagType:        TypeByte,
+		UnitCount:      6,
+		ValueOffset:    0x0,
+		RawValueOffset: []byte{0x0, 0x0, 0x0, 0x0},
+		IfdPath:        IfdPathStandard,
+	}
+
+	expected := "IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x0001) TAG-TYPE=[BYTE] UNIT-COUNT=(6)>"
+	if ite.String() != expected {
+		t.Fatalf("string representation not expected: [%s] != [%s]", ite.String(), expected)
+	}
+}
+
+func TestIfdTagEntryValueResolver_ValueBytes(t *testing.T) {
+	allocatedData := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
+
+	ite := IfdTagEntry{
+		TagId:          0x1,
+		TagIndex:       0,
+		TagType:        TypeByte,
+		UnitCount:      uint32(len(allocatedData)),
+		ValueOffset:    0x8,
+		RawValueOffset: []byte{0x0, 0x0, 0x0, 0x0},
+		IfdPath:        IfdPathStandard,
+	}
+
+	headerBytes, err := BuildExifHeader(TestDefaultByteOrder, uint32(0))
+	log.PanicIf(err)
+
+	exifData := make([]byte, len(headerBytes)+len(allocatedData))
+	copy(exifData[0:], headerBytes)
+	copy(exifData[len(headerBytes):], allocatedData)
+
+	itevr := NewIfdTagEntryValueResolver(exifData, TestDefaultByteOrder)
+
+	value, err := itevr.ValueBytes(&ite)
+	log.PanicIf(err)
+
+	if bytes.Compare(value, allocatedData) != 0 {
+		t.Fatalf("bytes not expected: %v != %v", value, allocatedData)
+	}
+}
diff --git a/v2/ifd_test.go b/v2/ifd_test.go
new file mode 100644
index 0000000..83481e8
--- /dev/null
+++ b/v2/ifd_test.go
@@ -0,0 +1,264 @@
+package exif
+
+import (
+	"fmt"
+	"reflect"
+	"sort"
+	"testing"
+
+	"github.com/dsoprea/go-logging"
+)
+
+func TestIfdMapping_Add(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := im.Add([]uint16{}, 0x1111, "ifd0")
+	log.PanicIf(err)
+
+	err = im.Add([]uint16{0x1111}, 0x4444, "ifd00")
+	log.PanicIf(err)
+
+	err = im.Add([]uint16{0x1111, 0x4444}, 0x5555, "ifd000")
+	log.PanicIf(err)
+
+	err = im.Add([]uint16{}, 0x2222, "ifd1")
+	log.PanicIf(err)
+
+	err = im.Add([]uint16{}, 0x3333, "ifd2")
+	log.PanicIf(err)
+
+	lineages, err := im.DumpLineages()
+	log.PanicIf(err)
+
+	sort.Strings(lineages)
+
+	expected := []string{
+		"ifd0",
+		"ifd0/ifd00",
+		"ifd0/ifd00/ifd000",
+		"ifd1",
+		"ifd2",
+	}
+
+	if reflect.DeepEqual(lineages, expected) != true {
+		fmt.Printf("Actual:\n")
+		fmt.Printf("\n")
+
+		for i, line := range lineages {
+			fmt.Printf("(%d) %s\n", i, line)
+		}
+
+		fmt.Printf("\n")
+
+		fmt.Printf("Expected:\n")
+		fmt.Printf("\n")
+
+		for i, line := range expected {
+			fmt.Printf("(%d) %s\n", i, line)
+		}
+
+		t.Fatalf("IFD-mapping dump not correct.")
+	}
+}
+
+func TestIfdMapping_LoadStandardIfds(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	lineages, err := im.DumpLineages()
+	log.PanicIf(err)
+
+	sort.Strings(lineages)
+
+	expected := []string{
+		"IFD",
+		"IFD/Exif",
+		"IFD/Exif/Iop",
+		"IFD/GPSInfo",
+	}
+
+	if reflect.DeepEqual(lineages, expected) != true {
+		fmt.Printf("Actual:\n")
+		fmt.Printf("\n")
+
+		for i, line := range lineages {
+			fmt.Printf("(%d) %s\n", i, line)
+		}
+
+		fmt.Printf("\n")
+
+		fmt.Printf("Expected:\n")
+		fmt.Printf("\n")
+
+		for i, line := range expected {
+			fmt.Printf("(%d) %s\n", i, line)
+		}
+
+		t.Fatalf("IFD-mapping dump not correct.")
+	}
+}
+
+func TestIfdMapping_Get(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	mi, err := im.Get([]uint16{IfdRootId, IfdExifId, IfdIopId})
+	log.PanicIf(err)
+
+	if mi.ParentTagId != IfdExifId {
+		t.Fatalf("Parent tag-ID not correct")
+	} else if mi.TagId != IfdIopId {
+		t.Fatalf("Tag-ID not correct")
+	} else if mi.Name != "Iop" {
+		t.Fatalf("name not correct")
+	} else if mi.PathPhrase() != "IFD/Exif/Iop" {
+		t.Fatalf("path not correct")
+	}
+}
+
+func TestIfdMapping_GetWithPath(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	mi, err := im.GetWithPath("IFD/Exif/Iop")
+	log.PanicIf(err)
+
+	if mi.ParentTagId != IfdExifId {
+		t.Fatalf("Parent tag-ID not correct")
+	} else if mi.TagId != IfdIopId {
+		t.Fatalf("Tag-ID not correct")
+	} else if mi.Name != "Iop" {
+		t.Fatalf("name not correct")
+	} else if mi.PathPhrase() != "IFD/Exif/Iop" {
+		t.Fatalf("path not correct")
+	}
+}
+
+func TestIfdMapping_ResolvePath__Regular(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	lineage, err := im.ResolvePath("IFD/Exif/Iop")
+	log.PanicIf(err)
+
+	expected := []IfdTagIdAndIndex{
+		IfdTagIdAndIndex{Name: "IFD", TagId: 0, Index: 0},
+		IfdTagIdAndIndex{Name: "Exif", TagId: 0x8769, Index: 0},
+		IfdTagIdAndIndex{Name: "Iop", TagId: 0xa005, Index: 0},
+	}
+
+	if reflect.DeepEqual(lineage, expected) != true {
+		t.Fatalf("Lineage not correct.")
+	}
+}
+
+func TestIfdMapping_ResolvePath__WithIndices(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	lineage, err := im.ResolvePath("IFD/Exif1/Iop")
+	log.PanicIf(err)
+
+	expected := []IfdTagIdAndIndex{
+		IfdTagIdAndIndex{Name: "IFD", TagId: 0, Index: 0},
+		IfdTagIdAndIndex{Name: "Exif", TagId: 0x8769, Index: 1},
+		IfdTagIdAndIndex{Name: "Iop", TagId: 0xa005, Index: 0},
+	}
+
+	if reflect.DeepEqual(lineage, expected) != true {
+		t.Fatalf("Lineage not correct.")
+	}
+}
+
+func TestIfdMapping_ResolvePath__Miss(t *testing.T) {
+	im := NewIfdMapping()
+
+	err := LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	_, err = im.ResolvePath("IFD/Exif/Invalid")
+	if err == nil {
+		t.Fatalf("Expected failure for invalid IFD path.")
+	} else if err.Error() != "ifd child with name [Invalid] not registered: [IFD/Exif/Invalid]" {
+		log.Panic(err)
+	}
+}
+
+func TestIfdMapping_FqPathPhraseFromLineage(t *testing.T) {
+	lineage := []IfdTagIdAndIndex{
+		IfdTagIdAndIndex{Name: "IFD", Index: 0},
+		IfdTagIdAndIndex{Name: "Exif", Index: 1},
+		IfdTagIdAndIndex{Name: "Iop", Index: 0},
+	}
+
+	im := NewIfdMapping()
+
+	fqPathPhrase := im.FqPathPhraseFromLineage(lineage)
+	if fqPathPhrase != "IFD/Exif1/Iop" {
+		t.Fatalf("path-phrase not correct: [%s]", fqPathPhrase)
+	}
+}
+
+func TestIfdMapping_PathPhraseFromLineage(t *testing.T) {
+	lineage := []IfdTagIdAndIndex{
+		IfdTagIdAndIndex{Name: "IFD", Index: 0},
+		IfdTagIdAndIndex{Name: "Exif", Index: 1},
+		IfdTagIdAndIndex{Name: "Iop", Index: 0},
+	}
+
+	im := NewIfdMapping()
+
+	fqPathPhrase := im.PathPhraseFromLineage(lineage)
+	if fqPathPhrase != "IFD/Exif/Iop" {
+		t.Fatalf("path-phrase not correct: [%s]", fqPathPhrase)
+	}
+}
+
+func TestIfdMapping_NewIfdMappingWithStandard(t *testing.T) {
+	imWith := NewIfdMappingWithStandard()
+	imWithout := NewIfdMapping()
+
+	err := LoadStandardIfds(imWithout)
+
+	outputWith, err := imWith.DumpLineages()
+	log.PanicIf(err)
+
+	sort.Strings(outputWith)
+
+	outputWithout, err := imWithout.DumpLineages()
+	log.PanicIf(err)
+
+	sort.Strings(outputWithout)
+
+	if reflect.DeepEqual(outputWith, outputWithout) != true {
+		fmt.Printf("WITH:\n")
+		fmt.Printf("\n")
+
+		for _, line := range outputWith {
+			fmt.Printf("%s\n", line)
+		}
+
+		fmt.Printf("\n")
+
+		fmt.Printf("WITHOUT:\n")
+		fmt.Printf("\n")
+
+		for _, line := range outputWithout {
+			fmt.Printf("%s\n", line)
+		}
+
+		fmt.Printf("\n")
+
+		t.Fatalf("Standard IFDs not loaded correctly.")
+	}
+}
diff --git a/v2/parser.go b/v2/parser.go
new file mode 100644
index 0000000..4702db2
--- /dev/null
+++ b/v2/parser.go
@@ -0,0 +1,190 @@
+package exif
+
+import (
+	"bytes"
+
+	"encoding/binary"
+
+	"github.com/dsoprea/go-logging"
+)
+
+type Parser struct {
+}
+
+func (p *Parser) ParseBytes(data []byte, unitCount uint32) (value []uint8, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	count := int(unitCount)
+
+	if len(data) < (TypeByte.Size() * count) {
+		log.Panic(ErrNotEnoughData)
+	}
+
+	value = []uint8(data[:count])
+
+	return value, nil
+}
+
+// ParseAscii returns a string and auto-strips the trailing NUL character.
+func (p *Parser) ParseAscii(data []byte, unitCount uint32) (value string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	count := int(unitCount)
+
+	if len(data) < (TypeAscii.Size() * count) {
+		log.Panic(ErrNotEnoughData)
+	}
+
+	if len(data) == 0 || data[count-1] != 0 {
+		s := string(data[:count])
+		typeLogger.Warningf(nil, "ascii not terminated with nul as expected: [%v]", s)
+
+		return s, nil
+	} else {
+		// Auto-strip the NUL from the end. It serves no purpose outside of
+		// encoding semantics.
+
+		return string(data[:count-1]), nil
+	}
+}
+
+// ParseAsciiNoNul returns a string without any consideration for a trailing NUL
+// character.
+func (p *Parser) ParseAsciiNoNul(data []byte, unitCount uint32) (value string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	count := int(unitCount)
+
+	if len(data) < (TypeAscii.Size() * count) {
+		log.Panic(ErrNotEnoughData)
+	}
+
+	return string(data[:count]), nil
+}
+
+func (p *Parser) ParseShorts(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []uint16, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	count := int(unitCount)
+
+	if len(data) < (TypeShort.Size() * count) {
+		log.Panic(ErrNotEnoughData)
+	}
+
+	value = make([]uint16, count)
+	for i := 0; i < count; i++ {
+		value[i] = byteOrder.Uint16(data[i*2:])
+	}
+
+	return value, nil
+}
+
+func (p *Parser) ParseLongs(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []uint32, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	count := int(unitCount)
+
+	if len(data) < (TypeLong.Size() * count) {
+		log.Panic(ErrNotEnoughData)
+	}
+
+	value = make([]uint32, count)
+	for i := 0; i < count; i++ {
+		value[i] = byteOrder.Uint32(data[i*4:])
+	}
+
+	return value, nil
+}
+
+func (p *Parser) ParseRationals(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []Rational, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	count := int(unitCount)
+
+	if len(data) < (TypeRational.Size() * count) {
+		log.Panic(ErrNotEnoughData)
+	}
+
+	value = make([]Rational, count)
+	for i := 0; i < count; i++ {
+		value[i].Numerator = byteOrder.Uint32(data[i*8:])
+		value[i].Denominator = byteOrder.Uint32(data[i*8+4:])
+	}
+
+	return value, nil
+}
+
+func (p *Parser) ParseSignedLongs(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []int32, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	count := int(unitCount)
+
+	if len(data) < (TypeSignedLong.Size() * count) {
+		log.Panic(ErrNotEnoughData)
+	}
+
+	b := bytes.NewBuffer(data)
+
+	value = make([]int32, count)
+	for i := 0; i < count; i++ {
+		err := binary.Read(b, byteOrder, &value[i])
+		log.PanicIf(err)
+	}
+
+	return value, nil
+}
+
+func (p *Parser) ParseSignedRationals(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []SignedRational, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	count := int(unitCount)
+
+	if len(data) < (TypeSignedRational.Size() * count) {
+		log.Panic(ErrNotEnoughData)
+	}
+
+	b := bytes.NewBuffer(data)
+
+	value = make([]SignedRational, count)
+	for i := 0; i < count; i++ {
+		err = binary.Read(b, byteOrder, &value[i].Numerator)
+		log.PanicIf(err)
+
+		err = binary.Read(b, byteOrder, &value[i].Denominator)
+		log.PanicIf(err)
+	}
+
+	return value, nil
+}
diff --git a/v2/readme.go b/v2/readme.go
new file mode 100644
index 0000000..18b22bd
--- /dev/null
+++ b/v2/readme.go
@@ -0,0 +1,8 @@
+// exif parses raw EXIF information given a block of raw EXIF data. It can also
+// construct new EXIF information, and provides tools for doing so. This package
+// is not involved with the parsing of particular file-formats.
+//
+// The EXIF data must first be extracted and then provided to us. Conversely,
+// when constructing new EXIF data, the caller is responsible for packaging
+// this in whichever format they require.
+package exif
diff --git a/v2/tag_type.go b/v2/tag_type.go
new file mode 100644
index 0000000..e53b1c4
--- /dev/null
+++ b/v2/tag_type.go
@@ -0,0 +1,397 @@
+package exif
+
+// NOTE(dustin): Most of this file encapsulates deprecated functionality and awaits being dumped in a future release.
+
+import (
+	"fmt"
+
+	"encoding/binary"
+
+	"github.com/dsoprea/go-logging"
+)
+
+type TagType struct {
+	tagType   TagTypePrimitive
+	name      string
+	byteOrder binary.ByteOrder
+}
+
+func NewTagType(tagType TagTypePrimitive, byteOrder binary.ByteOrder) TagType {
+	name, found := TypeNames[tagType]
+	if found == false {
+		log.Panicf("tag-type not valid: 0x%04x", tagType)
+	}
+
+	return TagType{
+		tagType:   tagType,
+		name:      name,
+		byteOrder: byteOrder,
+	}
+}
+
+func (tt TagType) String() string {
+	return fmt.Sprintf("TagType<NAME=[%s]>", tt.name)
+}
+
+func (tt TagType) Name() string {
+	return tt.name
+}
+
+func (tt TagType) Type() TagTypePrimitive {
+	return tt.tagType
+}
+
+func (tt TagType) ByteOrder() binary.ByteOrder {
+	return tt.byteOrder
+}
+
+func (tt TagType) Size() int {
+
+	// DEPRECATED(dustin): `(TagTypePrimitive).Size()` should be used, directly.
+
+	return tt.Type().Size()
+}
+
+// valueIsEmbedded will return a boolean indicating whether the value should be
+// found directly within the IFD entry or an offset to somewhere else.
+func (tt TagType) valueIsEmbedded(unitCount uint32) bool {
+	return (tt.tagType.Size() * int(unitCount)) <= 4
+}
+
+func (tt TagType) readRawEncoded(valueContext ValueContext) (rawBytes []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	unitSizeRaw := uint32(tt.tagType.Size())
+
+	if tt.valueIsEmbedded(valueContext.UnitCount()) == true {
+		byteLength := unitSizeRaw * valueContext.UnitCount()
+		return valueContext.RawValueOffset()[:byteLength], nil
+	} else {
+		return valueContext.AddressableData()[valueContext.ValueOffset() : valueContext.ValueOffset()+valueContext.UnitCount()*unitSizeRaw], nil
+	}
+}
+
+func (tt TagType) ParseBytes(data []byte, unitCount uint32) (value []uint8, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(*Parser).ParseBytes()` should be used.
+
+	value, err = parser.ParseBytes(data, unitCount)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+// ParseAscii returns a string and auto-strips the trailing NUL character.
+func (tt TagType) ParseAscii(data []byte, unitCount uint32) (value string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(*Parser).ParseAscii()` should be used.
+
+	value, err = parser.ParseAscii(data, unitCount)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+// ParseAsciiNoNul returns a string without any consideration for a trailing NUL
+// character.
+func (tt TagType) ParseAsciiNoNul(data []byte, unitCount uint32) (value string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(*Parser).ParseAsciiNoNul()` should be used.
+
+	value, err = parser.ParseAsciiNoNul(data, unitCount)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (tt TagType) ParseShorts(data []byte, unitCount uint32) (value []uint16, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(*Parser).ParseShorts()` should be used.
+
+	value, err = parser.ParseShorts(data, unitCount, tt.byteOrder)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (tt TagType) ParseLongs(data []byte, unitCount uint32) (value []uint32, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(*Parser).ParseLongs()` should be used.
+
+	value, err = parser.ParseLongs(data, unitCount, tt.byteOrder)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (tt TagType) ParseRationals(data []byte, unitCount uint32) (value []Rational, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(*Parser).ParseRationals()` should be used.
+
+	value, err = parser.ParseRationals(data, unitCount, tt.byteOrder)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (tt TagType) ParseSignedLongs(data []byte, unitCount uint32) (value []int32, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(*Parser).ParseSignedLongs()` should be used.
+
+	value, err = parser.ParseSignedLongs(data, unitCount, tt.byteOrder)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (tt TagType) ParseSignedRationals(data []byte, unitCount uint32) (value []SignedRational, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(*Parser).ParseSignedRationals()` should be used.
+
+	value, err = parser.ParseSignedRationals(data, unitCount, tt.byteOrder)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (tt TagType) ReadByteValues(valueContext ValueContext) (value []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(ValueContext).ReadBytes()` should be used.
+
+	value, err = valueContext.ReadBytes()
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (tt TagType) ReadAsciiValue(valueContext ValueContext) (value string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(ValueContext).ReadAscii()` should be used.
+
+	value, err = valueContext.ReadAscii()
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (tt TagType) ReadAsciiNoNulValue(valueContext ValueContext) (value string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(ValueContext).ReadAsciiNoNul()` should be used.
+
+	value, err = valueContext.ReadAsciiNoNul()
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (tt TagType) ReadShortValues(valueContext ValueContext) (value []uint16, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(ValueContext).ReadShorts()` should be used.
+
+	value, err = valueContext.ReadShorts()
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (tt TagType) ReadLongValues(valueContext ValueContext) (value []uint32, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(ValueContext).ReadLongs()` should be used.
+
+	value, err = valueContext.ReadLongs()
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (tt TagType) ReadRationalValues(valueContext ValueContext) (value []Rational, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(ValueContext).ReadRationals()` should be used.
+
+	value, err = valueContext.ReadRationals()
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (tt TagType) ReadSignedLongValues(valueContext ValueContext) (value []int32, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(ValueContext).ReadSignedLongs()` should be used.
+
+	value, err = valueContext.ReadSignedLongs()
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (tt TagType) ReadSignedRationalValues(valueContext ValueContext) (value []SignedRational, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(ValueContext).ReadSignedRationals()` should be used.
+
+	value, err = valueContext.ReadSignedRationals()
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+// ResolveAsString resolves the given value and returns a flat string.
+//
+// Where the type is not ASCII, `justFirst` indicates whether to just stringify
+// the first item in the slice (or return an empty string if the slice is
+// empty).
+//
+// Since this method lacks the information to process unknown-type tags (e.g.
+// byte-order, tag-ID, IFD type), it will return an error if attempted. See
+// `Undefined()`.
+func (tt TagType) ResolveAsString(valueContext ValueContext, justFirst bool) (value string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	if justFirst == true {
+		value, err = valueContext.FormatFirst()
+		log.PanicIf(err)
+	} else {
+		value, err = valueContext.Format()
+		log.PanicIf(err)
+	}
+
+	return value, nil
+}
+
+// Resolve knows how to resolve the given value.
+//
+// Since this method lacks the information to process unknown-type tags (e.g.
+// byte-order, tag-ID, IFD type), it will return an error if attempted. See
+// `Undefined()`.
+func (tt TagType) Resolve(valueContext *ValueContext) (values interface{}, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `(ValueContext).Values()` should be used.
+
+	values, err = valueContext.Values()
+	log.PanicIf(err)
+
+	return values, nil
+}
+
+// Encode knows how to encode the given value to a byte slice.
+func (tt TagType) Encode(value interface{}) (encoded []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	ve := NewValueEncoder(tt.byteOrder)
+
+	ed, err := ve.EncodeWithType(tt, value)
+	log.PanicIf(err)
+
+	return ed.Encoded, err
+}
+
+func (tt TagType) FromString(valueString string) (value interface{}, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// DEPRECATED(dustin): `EncodeStringToBytes()` should be used.
+
+	value, err = EncodeStringToBytes(tt.tagType, valueString)
+	log.PanicIf(err)
+
+	return value, nil
+}
diff --git a/v2/tags.go b/v2/tags.go
new file mode 100644
index 0000000..ec333ec
--- /dev/null
+++ b/v2/tags.go
@@ -0,0 +1,229 @@
+package exif
+
+import (
+	"fmt"
+
+	"github.com/dsoprea/go-logging"
+	"gopkg.in/yaml.v2"
+)
+
+const (
+	// IFD1
+
+	ThumbnailOffsetTagId = 0x0201
+	ThumbnailSizeTagId   = 0x0202
+
+	// Exif
+
+	TagVersionId = 0x0000
+
+	TagLatitudeId     = 0x0002
+	TagLatitudeRefId  = 0x0001
+	TagLongitudeId    = 0x0004
+	TagLongitudeRefId = 0x0003
+
+	TagTimestampId = 0x0007
+	TagDatestampId = 0x001d
+
+	TagAltitudeId    = 0x0006
+	TagAltitudeRefId = 0x0005
+)
+
+var (
+	// tagsWithoutAlignment is a tag-lookup for tags whose value size won't
+	// necessarily be a multiple of its tag-type.
+	tagsWithoutAlignment = map[uint16]struct{}{
+		// The thumbnail offset is stored as a long, but its data is a binary
+		// blob (not a slice of longs).
+		ThumbnailOffsetTagId: struct{}{},
+	}
+)
+
+var (
+	tagsLogger = log.NewLogger("exif.tags")
+)
+
+// File structures.
+
+type encodedTag struct {
+	// id is signed, here, because YAML doesn't have enough information to
+	// support unsigned.
+	Id       int    `yaml:"id"`
+	Name     string `yaml:"name"`
+	TypeName string `yaml:"type_name"`
+}
+
+// Indexing structures.
+
+type IndexedTag struct {
+	Id      uint16
+	Name    string
+	IfdPath string
+	Type    TagTypePrimitive
+}
+
+func (it *IndexedTag) String() string {
+	return fmt.Sprintf("TAG<ID=(0x%04x) NAME=[%s] IFD=[%s]>", it.Id, it.Name, it.IfdPath)
+}
+
+func (it *IndexedTag) IsName(ifdPath, name string) bool {
+	return it.Name == name && it.IfdPath == ifdPath
+}
+
+func (it *IndexedTag) Is(ifdPath string, id uint16) bool {
+	return it.Id == id && it.IfdPath == ifdPath
+}
+
+type TagIndex struct {
+	tagsByIfd  map[string]map[uint16]*IndexedTag
+	tagsByIfdR map[string]map[string]*IndexedTag
+}
+
+func NewTagIndex() *TagIndex {
+	ti := new(TagIndex)
+
+	ti.tagsByIfd = make(map[string]map[uint16]*IndexedTag)
+	ti.tagsByIfdR = make(map[string]map[string]*IndexedTag)
+
+	return ti
+}
+
+func (ti *TagIndex) Add(it *IndexedTag) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// Store by ID.
+
+	family, found := ti.tagsByIfd[it.IfdPath]
+	if found == false {
+		family = make(map[uint16]*IndexedTag)
+		ti.tagsByIfd[it.IfdPath] = family
+	}
+
+	if _, found := family[it.Id]; found == true {
+		log.Panicf("tag-ID defined more than once for IFD [%s]: (%02x)", it.IfdPath, it.Id)
+	}
+
+	family[it.Id] = it
+
+	// Store by name.
+
+	familyR, found := ti.tagsByIfdR[it.IfdPath]
+	if found == false {
+		familyR = make(map[string]*IndexedTag)
+		ti.tagsByIfdR[it.IfdPath] = familyR
+	}
+
+	if _, found := familyR[it.Name]; found == true {
+		log.Panicf("tag-name defined more than once for IFD [%s]: (%s)", it.IfdPath, it.Name)
+	}
+
+	familyR[it.Name] = it
+
+	return nil
+}
+
+// Get returns information about the non-IFD tag.
+func (ti *TagIndex) Get(ifdPath string, id uint16) (it *IndexedTag, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	if len(ti.tagsByIfd) == 0 {
+		err := LoadStandardTags(ti)
+		log.PanicIf(err)
+	}
+
+	family, found := ti.tagsByIfd[ifdPath]
+	if found == false {
+		log.Panic(ErrTagNotFound)
+	}
+
+	it, found = family[id]
+	if found == false {
+		log.Panic(ErrTagNotFound)
+	}
+
+	return it, nil
+}
+
+// Get returns information about the non-IFD tag.
+func (ti *TagIndex) GetWithName(ifdPath string, name string) (it *IndexedTag, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	if len(ti.tagsByIfdR) == 0 {
+		err := LoadStandardTags(ti)
+		log.PanicIf(err)
+	}
+
+	it, found := ti.tagsByIfdR[ifdPath][name]
+	if found != true {
+		log.Panic(ErrTagNotFound)
+	}
+
+	return it, nil
+}
+
+// LoadStandardTags registers the tags that all devices/applications should
+// support.
+func LoadStandardTags(ti *TagIndex) (err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// Read static data.
+
+	encodedIfds := make(map[string][]encodedTag)
+
+	err = yaml.Unmarshal([]byte(tagsYaml), encodedIfds)
+	log.PanicIf(err)
+
+	// Load structure.
+
+	count := 0
+	for ifdPath, tags := range encodedIfds {
+		for _, tagInfo := range tags {
+			tagId := uint16(tagInfo.Id)
+			tagName := tagInfo.Name
+			tagTypeName := tagInfo.TypeName
+
+			// TODO(dustin): !! Non-standard types, but found in real data. Ignore for right now.
+			if tagTypeName == "SSHORT" || tagTypeName == "FLOAT" || tagTypeName == "DOUBLE" {
+				continue
+			}
+
+			tagTypeId, found := TypeNamesR[tagTypeName]
+			if found == false {
+				log.Panicf("type [%s] for [%s] not valid", tagTypeName, tagName)
+				continue
+			}
+
+			it := &IndexedTag{
+				IfdPath: ifdPath,
+				Id:      tagId,
+				Name:    tagName,
+				Type:    tagTypeId,
+			}
+
+			err = ti.Add(it)
+			log.PanicIf(err)
+
+			count++
+		}
+	}
+
+	tagsLogger.Debugf(nil, "(%d) tags loaded.", count)
+
+	return nil
+}
diff --git a/v2/tags_data.go b/v2/tags_data.go
new file mode 100644
index 0000000..64ec458
--- /dev/null
+++ b/v2/tags_data.go
@@ -0,0 +1,951 @@
+package exif
+
+var (
+	// From assets/tags.yaml . Needs to be here so it's embedded in the binary.
+	tagsYaml = `
+# Notes:
+#
+# This file was produced from http://www.exiv2.org/tags.html, using the included
+# tool, though that document appears to have some duplicates when all IDs are
+# supposed to be unique (EXIF information only has IDs, not IFDs; IFDs are
+# determined by our pre-existing knowledge of those tags).
+#
+# The webpage that we've produced this file from appears to indicate that
+# ImageWidth is represented by both 0x0100 and 0x0001 depending on whether the
+# encoding is RGB or YCbCr.
+IFD/Exif:
+- id: 0x829a
+  name: ExposureTime
+  type_name: RATIONAL
+- id: 0x829d
+  name: FNumber
+  type_name: RATIONAL
+- id: 0x8822
+  name: ExposureProgram
+  type_name: SHORT
+- id: 0x8824
+  name: SpectralSensitivity
+  type_name: ASCII
+- id: 0x8827
+  name: ISOSpeedRatings
+  type_name: SHORT
+- id: 0x8828
+  name: OECF
+  type_name: UNDEFINED
+- id: 0x8830
+  name: SensitivityType
+  type_name: SHORT
+- id: 0x8831
+  name: StandardOutputSensitivity
+  type_name: LONG
+- id: 0x8832
+  name: RecommendedExposureIndex
+  type_name: LONG
+- id: 0x8833
+  name: ISOSpeed
+  type_name: LONG
+- id: 0x8834
+  name: ISOSpeedLatitudeyyy
+  type_name: LONG
+- id: 0x8835
+  name: ISOSpeedLatitudezzz
+  type_name: LONG
+- id: 0x9000
+  name: ExifVersion
+  type_name: UNDEFINED
+- id: 0x9003
+  name: DateTimeOriginal
+  type_name: ASCII
+- id: 0x9004
+  name: DateTimeDigitized
+  type_name: ASCII
+- id: 0x9101
+  name: ComponentsConfiguration
+  type_name: UNDEFINED
+- id: 0x9102
+  name: CompressedBitsPerPixel
+  type_name: RATIONAL
+- id: 0x9201
+  name: ShutterSpeedValue
+  type_name: SRATIONAL
+- id: 0x9202
+  name: ApertureValue
+  type_name: RATIONAL
+- id: 0x9203
+  name: BrightnessValue
+  type_name: SRATIONAL
+- id: 0x9204
+  name: ExposureBiasValue
+  type_name: SRATIONAL
+- id: 0x9205
+  name: MaxApertureValue
+  type_name: RATIONAL
+- id: 0x9206
+  name: SubjectDistance
+  type_name: RATIONAL
+- id: 0x9207
+  name: MeteringMode
+  type_name: SHORT
+- id: 0x9208
+  name: LightSource
+  type_name: SHORT
+- id: 0x9209
+  name: Flash
+  type_name: SHORT
+- id: 0x920a
+  name: FocalLength
+  type_name: RATIONAL
+- id: 0x9214
+  name: SubjectArea
+  type_name: SHORT
+- id: 0x927c
+  name: MakerNote
+  type_name: UNDEFINED
+- id: 0x9286
+  name: UserComment
+  type_name: UNDEFINED
+- id: 0x9290
+  name: SubSecTime
+  type_name: ASCII
+- id: 0x9291
+  name: SubSecTimeOriginal
+  type_name: ASCII
+- id: 0x9292
+  name: SubSecTimeDigitized
+  type_name: ASCII
+- id: 0xa000
+  name: FlashpixVersion
+  type_name: UNDEFINED
+- id: 0xa001
+  name: ColorSpace
+  type_name: SHORT
+- id: 0xa002
+  name: PixelXDimension
+  type_name: LONG
+- id: 0xa003
+  name: PixelYDimension
+  type_name: LONG
+- id: 0xa004
+  name: RelatedSoundFile
+  type_name: ASCII
+- id: 0xa005
+  name: InteroperabilityTag
+  type_name: LONG
+- id: 0xa20b
+  name: FlashEnergy
+  type_name: RATIONAL
+- id: 0xa20c
+  name: SpatialFrequencyResponse
+  type_name: UNDEFINED
+- id: 0xa20e
+  name: FocalPlaneXResolution
+  type_name: RATIONAL
+- id: 0xa20f
+  name: FocalPlaneYResolution
+  type_name: RATIONAL
+- id: 0xa210
+  name: FocalPlaneResolutionUnit
+  type_name: SHORT
+- id: 0xa214
+  name: SubjectLocation
+  type_name: SHORT
+- id: 0xa215
+  name: ExposureIndex
+  type_name: RATIONAL
+- id: 0xa217
+  name: SensingMethod
+  type_name: SHORT
+- id: 0xa300
+  name: FileSource
+  type_name: UNDEFINED
+- id: 0xa301
+  name: SceneType
+  type_name: UNDEFINED
+- id: 0xa302
+  name: CFAPattern
+  type_name: UNDEFINED
+- id: 0xa401
+  name: CustomRendered
+  type_name: SHORT
+- id: 0xa402
+  name: ExposureMode
+  type_name: SHORT
+- id: 0xa403
+  name: WhiteBalance
+  type_name: SHORT
+- id: 0xa404
+  name: DigitalZoomRatio
+  type_name: RATIONAL
+- id: 0xa405
+  name: FocalLengthIn35mmFilm
+  type_name: SHORT
+- id: 0xa406
+  name: SceneCaptureType
+  type_name: SHORT
+- id: 0xa407
+  name: GainControl
+  type_name: SHORT
+- id: 0xa408
+  name: Contrast
+  type_name: SHORT
+- id: 0xa409
+  name: Saturation
+  type_name: SHORT
+- id: 0xa40a
+  name: Sharpness
+  type_name: SHORT
+- id: 0xa40b
+  name: DeviceSettingDescription
+  type_name: UNDEFINED
+- id: 0xa40c
+  name: SubjectDistanceRange
+  type_name: SHORT
+- id: 0xa420
+  name: ImageUniqueID
+  type_name: ASCII
+- id: 0xa430
+  name: CameraOwnerName
+  type_name: ASCII
+- id: 0xa431
+  name: BodySerialNumber
+  type_name: ASCII
+- id: 0xa432
+  name: LensSpecification
+  type_name: RATIONAL
+- id: 0xa433
+  name: LensMake
+  type_name: ASCII
+- id: 0xa434
+  name: LensModel
+  type_name: ASCII
+- id: 0xa435
+  name: LensSerialNumber
+  type_name: ASCII
+IFD/GPSInfo:
+- id: 0x0000
+  name: GPSVersionID
+  type_name: BYTE
+- id: 0x0001
+  name: GPSLatitudeRef
+  type_name: ASCII
+- id: 0x0002
+  name: GPSLatitude
+  type_name: RATIONAL
+- id: 0x0003
+  name: GPSLongitudeRef
+  type_name: ASCII
+- id: 0x0004
+  name: GPSLongitude
+  type_name: RATIONAL
+- id: 0x0005
+  name: GPSAltitudeRef
+  type_name: BYTE
+- id: 0x0006
+  name: GPSAltitude
+  type_name: RATIONAL
+- id: 0x0007
+  name: GPSTimeStamp
+  type_name: RATIONAL
+- id: 0x0008
+  name: GPSSatellites
+  type_name: ASCII
+- id: 0x0009
+  name: GPSStatus
+  type_name: ASCII
+- id: 0x000a
+  name: GPSMeasureMode
+  type_name: ASCII
+- id: 0x000b
+  name: GPSDOP
+  type_name: RATIONAL
+- id: 0x000c
+  name: GPSSpeedRef
+  type_name: ASCII
+- id: 0x000d
+  name: GPSSpeed
+  type_name: RATIONAL
+- id: 0x000e
+  name: GPSTrackRef
+  type_name: ASCII
+- id: 0x000f
+  name: GPSTrack
+  type_name: RATIONAL
+- id: 0x0010
+  name: GPSImgDirectionRef
+  type_name: ASCII
+- id: 0x0011
+  name: GPSImgDirection
+  type_name: RATIONAL
+- id: 0x0012
+  name: GPSMapDatum
+  type_name: ASCII
+- id: 0x0013
+  name: GPSDestLatitudeRef
+  type_name: ASCII
+- id: 0x0014
+  name: GPSDestLatitude
+  type_name: RATIONAL
+- id: 0x0015
+  name: GPSDestLongitudeRef
+  type_name: ASCII
+- id: 0x0016
+  name: GPSDestLongitude
+  type_name: RATIONAL
+- id: 0x0017
+  name: GPSDestBearingRef
+  type_name: ASCII
+- id: 0x0018
+  name: GPSDestBearing
+  type_name: RATIONAL
+- id: 0x0019
+  name: GPSDestDistanceRef
+  type_name: ASCII
+- id: 0x001a
+  name: GPSDestDistance
+  type_name: RATIONAL
+- id: 0x001b
+  name: GPSProcessingMethod
+  type_name: UNDEFINED
+- id: 0x001c
+  name: GPSAreaInformation
+  type_name: UNDEFINED
+- id: 0x001d
+  name: GPSDateStamp
+  type_name: ASCII
+- id: 0x001e
+  name: GPSDifferential
+  type_name: SHORT
+IFD:
+- id: 0x000b
+  name: ProcessingSoftware
+  type_name: ASCII
+- id: 0x00fe
+  name: NewSubfileType
+  type_name: LONG
+- id: 0x00ff
+  name: SubfileType
+  type_name: SHORT
+- id: 0x0100
+  name: ImageWidth
+  type_name: LONG
+- id: 0x0101
+  name: ImageLength
+  type_name: LONG
+- id: 0x0102
+  name: BitsPerSample
+  type_name: SHORT
+- id: 0x0103
+  name: Compression
+  type_name: SHORT
+- id: 0x0106
+  name: PhotometricInterpretation
+  type_name: SHORT
+- id: 0x0107
+  name: Thresholding
+  type_name: SHORT
+- id: 0x0108
+  name: CellWidth
+  type_name: SHORT
+- id: 0x0109
+  name: CellLength
+  type_name: SHORT
+- id: 0x010a
+  name: FillOrder
+  type_name: SHORT
+- id: 0x010d
+  name: DocumentName
+  type_name: ASCII
+- id: 0x010e
+  name: ImageDescription
+  type_name: ASCII
+- id: 0x010f
+  name: Make
+  type_name: ASCII
+- id: 0x0110
+  name: Model
+  type_name: ASCII
+- id: 0x0111
+  name: StripOffsets
+  type_name: LONG
+- id: 0x0112
+  name: Orientation
+  type_name: SHORT
+- id: 0x0115
+  name: SamplesPerPixel
+  type_name: SHORT
+- id: 0x0116
+  name: RowsPerStrip
+  type_name: LONG
+- id: 0x0117
+  name: StripByteCounts
+  type_name: LONG
+- id: 0x011a
+  name: XResolution
+  type_name: RATIONAL
+- id: 0x011b
+  name: YResolution
+  type_name: RATIONAL
+- id: 0x011c
+  name: PlanarConfiguration
+  type_name: SHORT
+- id: 0x0122
+  name: GrayResponseUnit
+  type_name: SHORT
+- id: 0x0123
+  name: GrayResponseCurve
+  type_name: SHORT
+- id: 0x0124
+  name: T4Options
+  type_name: LONG
+- id: 0x0125
+  name: T6Options
+  type_name: LONG
+- id: 0x0128
+  name: ResolutionUnit
+  type_name: SHORT
+- id: 0x0129
+  name: PageNumber
+  type_name: SHORT
+- id: 0x012d
+  name: TransferFunction
+  type_name: SHORT
+- id: 0x0131
+  name: Software
+  type_name: ASCII
+- id: 0x0132
+  name: DateTime
+  type_name: ASCII
+- id: 0x013b
+  name: Artist
+  type_name: ASCII
+- id: 0x013c
+  name: HostComputer
+  type_name: ASCII
+- id: 0x013d
+  name: Predictor
+  type_name: SHORT
+- id: 0x013e
+  name: WhitePoint
+  type_name: RATIONAL
+- id: 0x013f
+  name: PrimaryChromaticities
+  type_name: RATIONAL
+- id: 0x0140
+  name: ColorMap
+  type_name: SHORT
+- id: 0x0141
+  name: HalftoneHints
+  type_name: SHORT
+- id: 0x0142
+  name: TileWidth
+  type_name: SHORT
+- id: 0x0143
+  name: TileLength
+  type_name: SHORT
+- id: 0x0144
+  name: TileOffsets
+  type_name: SHORT
+- id: 0x0145
+  name: TileByteCounts
+  type_name: SHORT
+- id: 0x014a
+  name: SubIFDs
+  type_name: LONG
+- id: 0x014c
+  name: InkSet
+  type_name: SHORT
+- id: 0x014d
+  name: InkNames
+  type_name: ASCII
+- id: 0x014e
+  name: NumberOfInks
+  type_name: SHORT
+- id: 0x0150
+  name: DotRange
+  type_name: BYTE
+- id: 0x0151
+  name: TargetPrinter
+  type_name: ASCII
+- id: 0x0152
+  name: ExtraSamples
+  type_name: SHORT
+- id: 0x0153
+  name: SampleFormat
+  type_name: SHORT
+- id: 0x0154
+  name: SMinSampleValue
+  type_name: SHORT
+- id: 0x0155
+  name: SMaxSampleValue
+  type_name: SHORT
+- id: 0x0156
+  name: TransferRange
+  type_name: SHORT
+- id: 0x0157
+  name: ClipPath
+  type_name: BYTE
+- id: 0x0158
+  name: XClipPathUnits
+  type_name: SSHORT
+- id: 0x0159
+  name: YClipPathUnits
+  type_name: SSHORT
+- id: 0x015a
+  name: Indexed
+  type_name: SHORT
+- id: 0x015b
+  name: JPEGTables
+  type_name: UNDEFINED
+- id: 0x015f
+  name: OPIProxy
+  type_name: SHORT
+- id: 0x0200
+  name: JPEGProc
+  type_name: LONG
+- id: 0x0201
+  name: JPEGInterchangeFormat
+  type_name: LONG
+- id: 0x0202
+  name: JPEGInterchangeFormatLength
+  type_name: LONG
+- id: 0x0203
+  name: JPEGRestartInterval
+  type_name: SHORT
+- id: 0x0205
+  name: JPEGLosslessPredictors
+  type_name: SHORT
+- id: 0x0206
+  name: JPEGPointTransforms
+  type_name: SHORT
+- id: 0x0207
+  name: JPEGQTables
+  type_name: LONG
+- id: 0x0208
+  name: JPEGDCTables
+  type_name: LONG
+- id: 0x0209
+  name: JPEGACTables
+  type_name: LONG
+- id: 0x0211
+  name: YCbCrCoefficients
+  type_name: RATIONAL
+- id: 0x0212
+  name: YCbCrSubSampling
+  type_name: SHORT
+- id: 0x0213
+  name: YCbCrPositioning
+  type_name: SHORT
+- id: 0x0214
+  name: ReferenceBlackWhite
+  type_name: RATIONAL
+- id: 0x02bc
+  name: XMLPacket
+  type_name: BYTE
+- id: 0x4746
+  name: Rating
+  type_name: SHORT
+- id: 0x4749
+  name: RatingPercent
+  type_name: SHORT
+- id: 0x800d
+  name: ImageID
+  type_name: ASCII
+- id: 0x828d
+  name: CFARepeatPatternDim
+  type_name: SHORT
+- id: 0x828e
+  name: CFAPattern
+  type_name: BYTE
+- id: 0x828f
+  name: BatteryLevel
+  type_name: RATIONAL
+- id: 0x8298
+  name: Copyright
+  type_name: ASCII
+- id: 0x829a
+  name: ExposureTime
+  type_name: RATIONAL
+- id: 0x829d
+  name: FNumber
+  type_name: RATIONAL
+- id: 0x83bb
+  name: IPTCNAA
+  type_name: LONG
+- id: 0x8649
+  name: ImageResources
+  type_name: BYTE
+- id: 0x8769
+  name: ExifTag
+  type_name: LONG
+- id: 0x8773
+  name: InterColorProfile
+  type_name: UNDEFINED
+- id: 0x8822
+  name: ExposureProgram
+  type_name: SHORT
+- id: 0x8824
+  name: SpectralSensitivity
+  type_name: ASCII
+- id: 0x8825
+  name: GPSTag
+  type_name: LONG
+- id: 0x8827
+  name: ISOSpeedRatings
+  type_name: SHORT
+- id: 0x8828
+  name: OECF
+  type_name: UNDEFINED
+- id: 0x8829
+  name: Interlace
+  type_name: SHORT
+- id: 0x882a
+  name: TimeZoneOffset
+  type_name: SSHORT
+- id: 0x882b
+  name: SelfTimerMode
+  type_name: SHORT
+- id: 0x9003
+  name: DateTimeOriginal
+  type_name: ASCII
+- id: 0x9102
+  name: CompressedBitsPerPixel
+  type_name: RATIONAL
+- id: 0x9201
+  name: ShutterSpeedValue
+  type_name: SRATIONAL
+- id: 0x9202
+  name: ApertureValue
+  type_name: RATIONAL
+- id: 0x9203
+  name: BrightnessValue
+  type_name: SRATIONAL
+- id: 0x9204
+  name: ExposureBiasValue
+  type_name: SRATIONAL
+- id: 0x9205
+  name: MaxApertureValue
+  type_name: RATIONAL
+- id: 0x9206
+  name: SubjectDistance
+  type_name: SRATIONAL
+- id: 0x9207
+  name: MeteringMode
+  type_name: SHORT
+- id: 0x9208
+  name: LightSource
+  type_name: SHORT
+- id: 0x9209
+  name: Flash
+  type_name: SHORT
+- id: 0x920a
+  name: FocalLength
+  type_name: RATIONAL
+- id: 0x920b
+  name: FlashEnergy
+  type_name: RATIONAL
+- id: 0x920c
+  name: SpatialFrequencyResponse
+  type_name: UNDEFINED
+- id: 0x920d
+  name: Noise
+  type_name: UNDEFINED
+- id: 0x920e
+  name: FocalPlaneXResolution
+  type_name: RATIONAL
+- id: 0x920f
+  name: FocalPlaneYResolution
+  type_name: RATIONAL
+- id: 0x9210
+  name: FocalPlaneResolutionUnit
+  type_name: SHORT
+- id: 0x9211
+  name: ImageNumber
+  type_name: LONG
+- id: 0x9212
+  name: SecurityClassification
+  type_name: ASCII
+- id: 0x9213
+  name: ImageHistory
+  type_name: ASCII
+- id: 0x9214
+  name: SubjectLocation
+  type_name: SHORT
+- id: 0x9215
+  name: ExposureIndex
+  type_name: RATIONAL
+- id: 0x9216
+  name: TIFFEPStandardID
+  type_name: BYTE
+- id: 0x9217
+  name: SensingMethod
+  type_name: SHORT
+- id: 0x9c9b
+  name: XPTitle
+  type_name: BYTE
+- id: 0x9c9c
+  name: XPComment
+  type_name: BYTE
+- id: 0x9c9d
+  name: XPAuthor
+  type_name: BYTE
+- id: 0x9c9e
+  name: XPKeywords
+  type_name: BYTE
+- id: 0x9c9f
+  name: XPSubject
+  type_name: BYTE
+- id: 0xc4a5
+  name: PrintImageMatching
+  type_name: UNDEFINED
+- id: 0xc612
+  name: DNGVersion
+  type_name: BYTE
+- id: 0xc613
+  name: DNGBackwardVersion
+  type_name: BYTE
+- id: 0xc614
+  name: UniqueCameraModel
+  type_name: ASCII
+- id: 0xc615
+  name: LocalizedCameraModel
+  type_name: BYTE
+- id: 0xc616
+  name: CFAPlaneColor
+  type_name: BYTE
+- id: 0xc617
+  name: CFALayout
+  type_name: SHORT
+- id: 0xc618
+  name: LinearizationTable
+  type_name: SHORT
+- id: 0xc619
+  name: BlackLevelRepeatDim
+  type_name: SHORT
+- id: 0xc61a
+  name: BlackLevel
+  type_name: RATIONAL
+- id: 0xc61b
+  name: BlackLevelDeltaH
+  type_name: SRATIONAL
+- id: 0xc61c
+  name: BlackLevelDeltaV
+  type_name: SRATIONAL
+- id: 0xc61d
+  name: WhiteLevel
+  type_name: SHORT
+- id: 0xc61e
+  name: DefaultScale
+  type_name: RATIONAL
+- id: 0xc61f
+  name: DefaultCropOrigin
+  type_name: SHORT
+- id: 0xc620
+  name: DefaultCropSize
+  type_name: SHORT
+- id: 0xc621
+  name: ColorMatrix1
+  type_name: SRATIONAL
+- id: 0xc622
+  name: ColorMatrix2
+  type_name: SRATIONAL
+- id: 0xc623
+  name: CameraCalibration1
+  type_name: SRATIONAL
+- id: 0xc624
+  name: CameraCalibration2
+  type_name: SRATIONAL
+- id: 0xc625
+  name: ReductionMatrix1
+  type_name: SRATIONAL
+- id: 0xc626
+  name: ReductionMatrix2
+  type_name: SRATIONAL
+- id: 0xc627
+  name: AnalogBalance
+  type_name: RATIONAL
+- id: 0xc628
+  name: AsShotNeutral
+  type_name: SHORT
+- id: 0xc629
+  name: AsShotWhiteXY
+  type_name: RATIONAL
+- id: 0xc62a
+  name: BaselineExposure
+  type_name: SRATIONAL
+- id: 0xc62b
+  name: BaselineNoise
+  type_name: RATIONAL
+- id: 0xc62c
+  name: BaselineSharpness
+  type_name: RATIONAL
+- id: 0xc62d
+  name: BayerGreenSplit
+  type_name: LONG
+- id: 0xc62e
+  name: LinearResponseLimit
+  type_name: RATIONAL
+- id: 0xc62f
+  name: CameraSerialNumber
+  type_name: ASCII
+- id: 0xc630
+  name: LensInfo
+  type_name: RATIONAL
+- id: 0xc631
+  name: ChromaBlurRadius
+  type_name: RATIONAL
+- id: 0xc632
+  name: AntiAliasStrength
+  type_name: RATIONAL
+- id: 0xc633
+  name: ShadowScale
+  type_name: SRATIONAL
+- id: 0xc634
+  name: DNGPrivateData
+  type_name: BYTE
+- id: 0xc635
+  name: MakerNoteSafety
+  type_name: SHORT
+- id: 0xc65a
+  name: CalibrationIlluminant1
+  type_name: SHORT
+- id: 0xc65b
+  name: CalibrationIlluminant2
+  type_name: SHORT
+- id: 0xc65c
+  name: BestQualityScale
+  type_name: RATIONAL
+- id: 0xc65d
+  name: RawDataUniqueID
+  type_name: BYTE
+- id: 0xc68b
+  name: OriginalRawFileName
+  type_name: BYTE
+- id: 0xc68c
+  name: OriginalRawFileData
+  type_name: UNDEFINED
+- id: 0xc68d
+  name: ActiveArea
+  type_name: SHORT
+- id: 0xc68e
+  name: MaskedAreas
+  type_name: SHORT
+- id: 0xc68f
+  name: AsShotICCProfile
+  type_name: UNDEFINED
+- id: 0xc690
+  name: AsShotPreProfileMatrix
+  type_name: SRATIONAL
+- id: 0xc691
+  name: CurrentICCProfile
+  type_name: UNDEFINED
+- id: 0xc692
+  name: CurrentPreProfileMatrix
+  type_name: SRATIONAL
+- id: 0xc6bf
+  name: ColorimetricReference
+  type_name: SHORT
+- id: 0xc6f3
+  name: CameraCalibrationSignature
+  type_name: BYTE
+- id: 0xc6f4
+  name: ProfileCalibrationSignature
+  type_name: BYTE
+- id: 0xc6f6
+  name: AsShotProfileName
+  type_name: BYTE
+- id: 0xc6f7
+  name: NoiseReductionApplied
+  type_name: RATIONAL
+- id: 0xc6f8
+  name: ProfileName
+  type_name: BYTE
+- id: 0xc6f9
+  name: ProfileHueSatMapDims
+  type_name: LONG
+- id: 0xc6fa
+  name: ProfileHueSatMapData1
+  type_name: FLOAT
+- id: 0xc6fb
+  name: ProfileHueSatMapData2
+  type_name: FLOAT
+- id: 0xc6fc
+  name: ProfileToneCurve
+  type_name: FLOAT
+- id: 0xc6fd
+  name: ProfileEmbedPolicy
+  type_name: LONG
+- id: 0xc6fe
+  name: ProfileCopyright
+  type_name: BYTE
+- id: 0xc714
+  name: ForwardMatrix1
+  type_name: SRATIONAL
+- id: 0xc715
+  name: ForwardMatrix2
+  type_name: SRATIONAL
+- id: 0xc716
+  name: PreviewApplicationName
+  type_name: BYTE
+- id: 0xc717
+  name: PreviewApplicationVersion
+  type_name: BYTE
+- id: 0xc718
+  name: PreviewSettingsName
+  type_name: BYTE
+- id: 0xc719
+  name: PreviewSettingsDigest
+  type_name: BYTE
+- id: 0xc71a
+  name: PreviewColorSpace
+  type_name: LONG
+- id: 0xc71b
+  name: PreviewDateTime
+  type_name: ASCII
+- id: 0xc71c
+  name: RawImageDigest
+  type_name: UNDEFINED
+- id: 0xc71d
+  name: OriginalRawFileDigest
+  type_name: UNDEFINED
+- id: 0xc71e
+  name: SubTileBlockSize
+  type_name: LONG
+- id: 0xc71f
+  name: RowInterleaveFactor
+  type_name: LONG
+- id: 0xc725
+  name: ProfileLookTableDims
+  type_name: LONG
+- id: 0xc726
+  name: ProfileLookTableData
+  type_name: FLOAT
+- id: 0xc740
+  name: OpcodeList1
+  type_name: UNDEFINED
+- id: 0xc741
+  name: OpcodeList2
+  type_name: UNDEFINED
+- id: 0xc74e
+  name: OpcodeList3
+  type_name: UNDEFINED
+- id: 0xc761
+  name: NoiseProfile
+  type_name: DOUBLE
+IFD/Exif/Iop:
+- id: 0x0001
+  name: InteroperabilityIndex
+  type_name: ASCII
+- id: 0x0002
+  name: InteroperabilityVersion
+  type_name: UNDEFINED
+- id: 0x1000
+  name: RelatedImageFileFormat
+  type_name: ASCII
+- id: 0x1001
+  name: RelatedImageWidth
+  type_name: LONG
+- id: 0x1002
+  name: RelatedImageLength
+  type_name: LONG
+`
+)
diff --git a/v2/tags_test.go b/v2/tags_test.go
new file mode 100644
index 0000000..01dcc65
--- /dev/null
+++ b/v2/tags_test.go
@@ -0,0 +1,29 @@
+package exif
+
+import (
+	"testing"
+
+	"github.com/dsoprea/go-logging"
+)
+
+func TestGet(t *testing.T) {
+	ti := NewTagIndex()
+
+	it, err := ti.Get(IfdPathStandard, 0x10f)
+	log.PanicIf(err)
+
+	if it.Is(IfdPathStandard, 0x10f) == false || it.IsName(IfdPathStandard, "Make") == false {
+		t.Fatalf("tag info not correct")
+	}
+}
+
+func TestGetWithName(t *testing.T) {
+	ti := NewTagIndex()
+
+	it, err := ti.GetWithName(IfdPathStandard, "Make")
+	log.PanicIf(err)
+
+	if it.Is(IfdPathStandard, 0x10f) == false || it.Is(IfdPathStandard, 0x10f) == false {
+		t.Fatalf("tag info not correct")
+	}
+}
diff --git a/v2/tags_undefined.go b/v2/tags_undefined.go
new file mode 100644
index 0000000..3e752b6
--- /dev/null
+++ b/v2/tags_undefined.go
@@ -0,0 +1,417 @@
+package exif
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+
+	"crypto/sha1"
+	"encoding/binary"
+
+	"github.com/dsoprea/go-logging"
+)
+
+const (
+	UnparseableUnknownTagValuePlaceholder = "!UNKNOWN"
+)
+
+// TODO(dustin): Rename "unknown" in symbol names to "undefined" in the next release.
+//
+// See https://github.com/dsoprea/go-exif/issues/27 .
+
+const (
+	TagUnknownType_9298_UserComment_Encoding_ASCII     = iota
+	TagUnknownType_9298_UserComment_Encoding_JIS       = iota
+	TagUnknownType_9298_UserComment_Encoding_UNICODE   = iota
+	TagUnknownType_9298_UserComment_Encoding_UNDEFINED = iota
+)
+
+const (
+	TagUnknownType_9101_ComponentsConfiguration_Channel_Y  = 0x1
+	TagUnknownType_9101_ComponentsConfiguration_Channel_Cb = 0x2
+	TagUnknownType_9101_ComponentsConfiguration_Channel_Cr = 0x3
+	TagUnknownType_9101_ComponentsConfiguration_Channel_R  = 0x4
+	TagUnknownType_9101_ComponentsConfiguration_Channel_G  = 0x5
+	TagUnknownType_9101_ComponentsConfiguration_Channel_B  = 0x6
+)
+
+const (
+	TagUnknownType_9101_ComponentsConfiguration_OTHER = iota
+	TagUnknownType_9101_ComponentsConfiguration_RGB   = iota
+	TagUnknownType_9101_ComponentsConfiguration_YCBCR = iota
+)
+
+var (
+	TagUnknownType_9298_UserComment_Encoding_Names = map[int]string{
+		TagUnknownType_9298_UserComment_Encoding_ASCII:     "ASCII",
+		TagUnknownType_9298_UserComment_Encoding_JIS:       "JIS",
+		TagUnknownType_9298_UserComment_Encoding_UNICODE:   "UNICODE",
+		TagUnknownType_9298_UserComment_Encoding_UNDEFINED: "UNDEFINED",
+	}
+
+	TagUnknownType_9298_UserComment_Encodings = map[int][]byte{
+		TagUnknownType_9298_UserComment_Encoding_ASCII:     []byte{'A', 'S', 'C', 'I', 'I', 0, 0, 0},
+		TagUnknownType_9298_UserComment_Encoding_JIS:       []byte{'J', 'I', 'S', 0, 0, 0, 0, 0},
+		TagUnknownType_9298_UserComment_Encoding_UNICODE:   []byte{'U', 'n', 'i', 'c', 'o', 'd', 'e', 0},
+		TagUnknownType_9298_UserComment_Encoding_UNDEFINED: []byte{0, 0, 0, 0, 0, 0, 0, 0},
+	}
+
+	TagUnknownType_9101_ComponentsConfiguration_Names = map[int]string{
+		TagUnknownType_9101_ComponentsConfiguration_OTHER: "OTHER",
+		TagUnknownType_9101_ComponentsConfiguration_RGB:   "RGB",
+		TagUnknownType_9101_ComponentsConfiguration_YCBCR: "YCBCR",
+	}
+
+	TagUnknownType_9101_ComponentsConfiguration_Configurations = map[int][]byte{
+		TagUnknownType_9101_ComponentsConfiguration_RGB: []byte{
+			TagUnknownType_9101_ComponentsConfiguration_Channel_R,
+			TagUnknownType_9101_ComponentsConfiguration_Channel_G,
+			TagUnknownType_9101_ComponentsConfiguration_Channel_B,
+			0,
+		},
+
+		TagUnknownType_9101_ComponentsConfiguration_YCBCR: []byte{
+			TagUnknownType_9101_ComponentsConfiguration_Channel_Y,
+			TagUnknownType_9101_ComponentsConfiguration_Channel_Cb,
+			TagUnknownType_9101_ComponentsConfiguration_Channel_Cr,
+			0,
+		},
+	}
+)
+
+// TODO(dustin): Rename `UnknownTagValue` to `UndefinedTagValue`.
+
+type UnknownTagValue interface {
+	ValueBytes() ([]byte, error)
+}
+
+// TODO(dustin): Rename `TagUnknownType_GeneralString` to `TagUnknownType_GeneralString`.
+
+type TagUnknownType_GeneralString string
+
+func (gs TagUnknownType_GeneralString) ValueBytes() (value []byte, err error) {
+	return []byte(gs), nil
+}
+
+// TODO(dustin): Rename `TagUnknownType_9298_UserComment` to `TagUndefinedType_9298_UserComment`.
+
+type TagUnknownType_9298_UserComment struct {
+	EncodingType  int
+	EncodingBytes []byte
+}
+
+func (uc TagUnknownType_9298_UserComment) String() string {
+	var valuePhrase string
+
+	if len(uc.EncodingBytes) <= 8 {
+		valuePhrase = fmt.Sprintf("%v", uc.EncodingBytes)
+	} else {
+		valuePhrase = fmt.Sprintf("%v...", uc.EncodingBytes[:8])
+	}
+
+	return fmt.Sprintf("UserComment<SIZE=(%d) ENCODING=[%s] V=%v LEN=(%d)>", len(uc.EncodingBytes), TagUnknownType_9298_UserComment_Encoding_Names[uc.EncodingType], valuePhrase, len(uc.EncodingBytes))
+}
+
+func (uc TagUnknownType_9298_UserComment) ValueBytes() (value []byte, err error) {
+	encodingTypeBytes, found := TagUnknownType_9298_UserComment_Encodings[uc.EncodingType]
+	if found == false {
+		log.Panicf("encoding-type not valid for unknown-type tag 9298 (UserComment): (%d)", uc.EncodingType)
+	}
+
+	value = make([]byte, len(uc.EncodingBytes)+8)
+
+	copy(value[:8], encodingTypeBytes)
+	copy(value[8:], uc.EncodingBytes)
+
+	return value, nil
+}
+
+// TODO(dustin): Rename `TagUnknownType_927C_MakerNote` to `TagUndefinedType_927C_MakerNote`.
+
+type TagUnknownType_927C_MakerNote struct {
+	MakerNoteType  []byte
+	MakerNoteBytes []byte
+}
+
+func (mn TagUnknownType_927C_MakerNote) String() string {
+	parts := make([]string, 20)
+	for i, c := range mn.MakerNoteType {
+		parts[i] = fmt.Sprintf("%02x", c)
+	}
+
+	h := sha1.New()
+
+	_, err := h.Write(mn.MakerNoteBytes)
+	log.PanicIf(err)
+
+	digest := h.Sum(nil)
+
+	return fmt.Sprintf("MakerNote<TYPE-ID=[%s] LEN=(%d) SHA1=[%020x]>", strings.Join(parts, " "), len(mn.MakerNoteBytes), digest)
+}
+
+func (uc TagUnknownType_927C_MakerNote) ValueBytes() (value []byte, err error) {
+	return uc.MakerNoteBytes, nil
+}
+
+// TODO(dustin): Rename `TagUnknownType_9101_ComponentsConfiguration` to `TagUndefinedType_9101_ComponentsConfiguration`.
+
+type TagUnknownType_9101_ComponentsConfiguration struct {
+	ConfigurationId    int
+	ConfigurationBytes []byte
+}
+
+func (cc TagUnknownType_9101_ComponentsConfiguration) String() string {
+	return fmt.Sprintf("ComponentsConfiguration<ID=[%s] BYTES=%v>", TagUnknownType_9101_ComponentsConfiguration_Names[cc.ConfigurationId], cc.ConfigurationBytes)
+}
+
+func (uc TagUnknownType_9101_ComponentsConfiguration) ValueBytes() (value []byte, err error) {
+	return uc.ConfigurationBytes, nil
+}
+
+// TODO(dustin): Rename `EncodeUnknown_9286` to `EncodeUndefined_9286`.
+
+func EncodeUnknown_9286(uc TagUnknownType_9298_UserComment) (encoded []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	b := new(bytes.Buffer)
+
+	encodingTypeBytes := TagUnknownType_9298_UserComment_Encodings[uc.EncodingType]
+
+	_, err = b.Write(encodingTypeBytes)
+	log.PanicIf(err)
+
+	_, err = b.Write(uc.EncodingBytes)
+	log.PanicIf(err)
+
+	return b.Bytes(), nil
+}
+
+type EncodeableUndefinedValue struct {
+	IfdPath    string
+	TagId      uint16
+	Parameters interface{}
+}
+
+func EncodeUndefined(ifdPath string, tagId uint16, value interface{}) (ed EncodedData, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// TODO(dustin): !! Finish implementing these.
+	if ifdPath == IfdPathStandardExif {
+		if tagId == 0x9286 {
+			encoded, err := EncodeUnknown_9286(value.(TagUnknownType_9298_UserComment))
+			log.PanicIf(err)
+
+			ed.Type = TypeUndefined
+			ed.Encoded = encoded
+			ed.UnitCount = uint32(len(encoded))
+
+			return ed, nil
+		}
+	}
+
+	log.Panicf("undefined value not encodable: %s (0x%02x)", ifdPath, tagId)
+
+	// Never called.
+	return EncodedData{}, nil
+}
+
+// TODO(dustin): Rename `TagUnknownType_UnknownValue` to `TagUndefinedType_UnknownValue`.
+
+type TagUnknownType_UnknownValue []byte
+
+func (tutuv TagUnknownType_UnknownValue) String() string {
+	parts := make([]string, len(tutuv))
+	for i, c := range tutuv {
+		parts[i] = fmt.Sprintf("%02x", c)
+	}
+
+	h := sha1.New()
+
+	_, err := h.Write(tutuv)
+	log.PanicIf(err)
+
+	digest := h.Sum(nil)
+
+	return fmt.Sprintf("Unknown<DATA=[%s] LEN=(%d) SHA1=[%020x]>", strings.Join(parts, " "), len(tutuv), digest)
+}
+
+// UndefinedValue knows how to resolve the value for most unknown-type tags.
+func UndefinedValue(ifdPath string, tagId uint16, valueContext interface{}, byteOrder binary.ByteOrder) (value interface{}, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	// TODO(dustin): Stop exporting this. Use `(*ValueContext).Undefined()`.
+
+	var valueContextPtr *ValueContext
+
+	if vc, ok := valueContext.(*ValueContext); ok == true {
+		// Legacy usage.
+
+		valueContextPtr = vc
+	} else {
+		// Standard usage.
+
+		valueContextValue := valueContext.(ValueContext)
+		valueContextPtr = &valueContextValue
+	}
+
+	typeLogger.Debugf(nil, "UndefinedValue: IFD-PATH=[%s] TAG-ID=(0x%02x)", ifdPath, tagId)
+
+	if ifdPath == IfdPathStandardExif {
+		if tagId == 0x9000 {
+			// ExifVersion
+
+			valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
+
+			valueString, err := valueContextPtr.ReadAsciiNoNul()
+			log.PanicIf(err)
+
+			return TagUnknownType_GeneralString(valueString), nil
+		} else if tagId == 0xa000 {
+			// FlashpixVersion
+
+			valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
+
+			valueString, err := valueContextPtr.ReadAsciiNoNul()
+			log.PanicIf(err)
+
+			return TagUnknownType_GeneralString(valueString), nil
+		} else if tagId == 0x9286 {
+			// UserComment
+
+			valueContextPtr.SetUnknownValueType(TypeByte)
+
+			valueBytes, err := valueContextPtr.ReadBytes()
+			log.PanicIf(err)
+
+			unknownUc := TagUnknownType_9298_UserComment{
+				EncodingType:  TagUnknownType_9298_UserComment_Encoding_UNDEFINED,
+				EncodingBytes: []byte{},
+			}
+
+			encoding := valueBytes[:8]
+			for encodingIndex, encodingBytes := range TagUnknownType_9298_UserComment_Encodings {
+				if bytes.Compare(encoding, encodingBytes) == 0 {
+					uc := TagUnknownType_9298_UserComment{
+						EncodingType:  encodingIndex,
+						EncodingBytes: valueBytes[8:],
+					}
+
+					return uc, nil
+				}
+			}
+
+			typeLogger.Warningf(nil, "User-comment encoding not valid. Returning 'unknown' type (the default).")
+			return unknownUc, nil
+		} else if tagId == 0x927c {
+			// MakerNote
+			// TODO(dustin): !! This is the Wild Wild West. This very well might be a child IFD, but any and all OEM's define their own formats. If we're going to be writing changes and this is complete EXIF (which may not have the first eight bytes), it might be fine. However, if these are just IFDs they'll be relative to the main EXIF, this will invalidate the MakerNote data for IFDs and any other implementations that use offsets unless we can interpret them all. It be best to return to this later and just exclude this from being written for now, though means a loss of a wealth of image metadata.
+			//                  -> We can also just blindly try to interpret as an IFD and just validate that it's looks good (maybe it will even have a 'next ifd' pointer that we can validate is 0x0).
+
+			valueContextPtr.SetUnknownValueType(TypeByte)
+
+			valueBytes, err := valueContextPtr.ReadBytes()
+			log.PanicIf(err)
+
+			// TODO(dustin): Doesn't work, but here as an example.
+			//             ie := NewIfdEnumerate(valueBytes, byteOrder)
+
+			// // TODO(dustin): !! Validate types (might have proprietary types, but it might be worth splitting the list between valid and not valid; maybe fail if a certain proportion are invalid, or maybe aren't less then a certain small integer)?
+			//             ii, err := ie.Collect(0x0)
+
+			//             for _, entry := range ii.RootIfd.Entries {
+			//                 fmt.Printf("ENTRY: 0x%02x %d\n", entry.TagId, entry.TagType)
+			//             }
+
+			mn := TagUnknownType_927C_MakerNote{
+				MakerNoteType: valueBytes[:20],
+
+				// MakerNoteBytes has the whole length of bytes. There's always
+				// the chance that the first 20 bytes includes actual data.
+				MakerNoteBytes: valueBytes,
+			}
+
+			return mn, nil
+		} else if tagId == 0x9101 {
+			// ComponentsConfiguration
+
+			valueContextPtr.SetUnknownValueType(TypeByte)
+
+			valueBytes, err := valueContextPtr.ReadBytes()
+			log.PanicIf(err)
+
+			for configurationId, configurationBytes := range TagUnknownType_9101_ComponentsConfiguration_Configurations {
+				if bytes.Compare(valueBytes, configurationBytes) == 0 {
+					cc := TagUnknownType_9101_ComponentsConfiguration{
+						ConfigurationId:    configurationId,
+						ConfigurationBytes: valueBytes,
+					}
+
+					return cc, nil
+				}
+			}
+
+			cc := TagUnknownType_9101_ComponentsConfiguration{
+				ConfigurationId:    TagUnknownType_9101_ComponentsConfiguration_OTHER,
+				ConfigurationBytes: valueBytes,
+			}
+
+			return cc, nil
+		}
+	} else if ifdPath == IfdPathStandardGps {
+		if tagId == 0x001c {
+			// GPSAreaInformation
+
+			valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
+
+			valueString, err := valueContextPtr.ReadAsciiNoNul()
+			log.PanicIf(err)
+
+			return TagUnknownType_GeneralString(valueString), nil
+		} else if tagId == 0x001b {
+			// GPSProcessingMethod
+
+			valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
+
+			valueString, err := valueContextPtr.ReadAsciiNoNul()
+			log.PanicIf(err)
+
+			return TagUnknownType_GeneralString(valueString), nil
+		}
+	} else if ifdPath == IfdPathStandardExifIop {
+		if tagId == 0x0002 {
+			// InteropVersion
+
+			valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
+
+			valueString, err := valueContextPtr.ReadAsciiNoNul()
+			log.PanicIf(err)
+
+			return TagUnknownType_GeneralString(valueString), nil
+		}
+	}
+
+	// TODO(dustin): !! Still need to do:
+	//
+	// complex: 0xa302, 0xa20c, 0x8828
+	// long: 0xa301, 0xa300
+	//
+	// 0xa40b is device-specific and unhandled.
+	//
+	// See https://github.com/dsoprea/go-exif/issues/26.
+
+	// We have no choice but to return the error. We have no way of knowing how
+	// much data there is without already knowing what data-type this tag is.
+	return nil, ErrUnhandledUnknownTypedTag
+}
diff --git a/v2/tags_undefined_test.go b/v2/tags_undefined_test.go
new file mode 100644
index 0000000..0403178
--- /dev/null
+++ b/v2/tags_undefined_test.go
@@ -0,0 +1,88 @@
+package exif
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/dsoprea/go-logging"
+)
+
+func TestUndefinedValue_ExifVersion(t *testing.T) {
+	byteOrder := TestDefaultByteOrder
+	fqIfdPath := "IFD0/Exif0"
+	ifdPath := "IFD/Exif"
+
+	// Create our unknown-type tag's value using the fact that we know it's a
+	// non-null-terminated string.
+
+	ve := NewValueEncoder(byteOrder)
+
+	tt := NewTagType(TypeAsciiNoNul, byteOrder)
+	valueString := "0230"
+
+	ed, err := ve.EncodeWithType(tt, valueString)
+	log.PanicIf(err)
+
+	// Create the tag using the official "unknown" type now that we already
+	// have the bytes.
+
+	encodedValue := NewIfdBuilderTagValueFromBytes(ed.Encoded)
+
+	bt := &BuilderTag{
+		ifdPath: ifdPath,
+		tagId:   0x9000,
+		typeId:  TypeUndefined,
+		value:   encodedValue,
+	}
+
+	// Stage the build.
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	ibe := NewIfdByteEncoder()
+	ib := NewIfdBuilder(im, ti, ifdPath, byteOrder)
+
+	b := new(bytes.Buffer)
+	bw := NewByteWriter(b, byteOrder)
+
+	addressableOffset := uint32(0x1234)
+	ida := newIfdDataAllocator(addressableOffset)
+
+	// Encode.
+
+	_, err = ibe.encodeTagToBytes(ib, bt, bw, ida, uint32(0))
+	log.PanicIf(err)
+
+	tagBytes := b.Bytes()
+
+	if len(tagBytes) != 12 {
+		t.Fatalf("Tag not encoded to the right number of bytes: (%d)", len(tagBytes))
+	}
+
+	ite, err := ParseOneTag(im, ti, fqIfdPath, ifdPath, byteOrder, tagBytes, false)
+	log.PanicIf(err)
+
+	if ite.TagId != 0x9000 {
+		t.Fatalf("Tag-ID not correct: (0x%02x)", ite.TagId)
+	} else if ite.TagIndex != 0 {
+		t.Fatalf("Tag index not correct: (%d)", ite.TagIndex)
+	} else if ite.TagType != TypeUndefined {
+		t.Fatalf("Tag type not correct: (%d)", ite.TagType)
+	} else if ite.UnitCount != (uint32(len(valueString))) {
+		t.Fatalf("Tag unit-count not correct: (%d)", ite.UnitCount)
+	} else if bytes.Compare(ite.RawValueOffset, []byte{'0', '2', '3', '0'}) != 0 {
+		t.Fatalf("Tag's value (as raw bytes) is not correct: [%x]", ite.RawValueOffset)
+	} else if ite.ChildIfdPath != "" {
+		t.Fatalf("Tag's child IFD-path should be empty: [%s]", ite.ChildIfdPath)
+	} else if ite.IfdPath != ifdPath {
+		t.Fatalf("Tag's parent IFD is not correct: %v", ite.IfdPath)
+	}
+}
+
+// TODO(dustin): !! Add tests for remaining, well-defined unknown
+// TODO(dustin): !! Test what happens with unhandled unknown-type tags (though it should never get to this point in the normal workflow).
diff --git a/v2/type.go b/v2/type.go
new file mode 100644
index 0000000..0d635b7
--- /dev/null
+++ b/v2/type.go
@@ -0,0 +1,310 @@
+package exif
+
+import (
+    "errors"
+    "fmt"
+    "strconv"
+    "strings"
+
+    "encoding/binary"
+
+    "github.com/dsoprea/go-logging"
+)
+
+type TagTypePrimitive uint16
+
+func (typeType TagTypePrimitive) String() string {
+    return TypeNames[typeType]
+}
+
+func (tagType TagTypePrimitive) Size() int {
+    if tagType == TypeByte {
+        return 1
+    } else if tagType == TypeAscii || tagType == TypeAsciiNoNul {
+        return 1
+    } else if tagType == TypeShort {
+        return 2
+    } else if tagType == TypeLong {
+        return 4
+    } else if tagType == TypeRational {
+        return 8
+    } else if tagType == TypeSignedLong {
+        return 4
+    } else if tagType == TypeSignedRational {
+        return 8
+    } else {
+        log.Panicf("can not determine tag-value size for type (%d): [%s]", tagType, TypeNames[tagType])
+
+        // Never called.
+        return 0
+    }
+}
+
+const (
+    TypeByte           TagTypePrimitive = 1
+    TypeAscii          TagTypePrimitive = 2
+    TypeShort          TagTypePrimitive = 3
+    TypeLong           TagTypePrimitive = 4
+    TypeRational       TagTypePrimitive = 5
+    TypeUndefined      TagTypePrimitive = 7
+    TypeSignedLong     TagTypePrimitive = 9
+    TypeSignedRational TagTypePrimitive = 10
+
+    // TypeAsciiNoNul is just a pseudo-type, for our own purposes.
+    TypeAsciiNoNul TagTypePrimitive = 0xf0
+)
+
+var (
+    typeLogger = log.NewLogger("exif.type")
+)
+
+var (
+    // TODO(dustin): Rename TypeNames() to typeNames() and add getter.
+    TypeNames = map[TagTypePrimitive]string{
+        TypeByte:           "BYTE",
+        TypeAscii:          "ASCII",
+        TypeShort:          "SHORT",
+        TypeLong:           "LONG",
+        TypeRational:       "RATIONAL",
+        TypeUndefined:      "UNDEFINED",
+        TypeSignedLong:     "SLONG",
+        TypeSignedRational: "SRATIONAL",
+
+        TypeAsciiNoNul: "_ASCII_NO_NUL",
+    }
+
+    TypeNamesR = map[string]TagTypePrimitive{}
+)
+
+var (
+    // ErrNotEnoughData is used when there isn't enough data to accomodate what
+    // we're trying to parse (sizeof(type) * unit_count).
+    ErrNotEnoughData = errors.New("not enough data for type")
+
+    // ErrWrongType is used when we try to parse anything other than the
+    // current type.
+    ErrWrongType = errors.New("wrong type, can not parse")
+
+    // ErrUnhandledUnknownTag is used when we try to parse a tag that's
+    // recorded as an "unknown" type but not a documented tag (therefore
+    // leaving us not knowning how to read it).
+    ErrUnhandledUnknownTypedTag = errors.New("not a standard unknown-typed tag")
+)
+
+type Rational struct {
+    Numerator   uint32
+    Denominator uint32
+}
+
+type SignedRational struct {
+    Numerator   int32
+    Denominator int32
+}
+
+func TagTypeSize(tagType TagTypePrimitive) int {
+
+    // DEPRECATED(dustin): `(TagTypePrimitive).Size()` should be used, directly.
+
+    return tagType.Size()
+}
+
+// Format returns a stringified value for the given bytes. Automatically
+// calculates count based on type size.
+func Format(rawBytes []byte, tagType TagTypePrimitive, justFirst bool, byteOrder binary.ByteOrder) (value string, err error) {
+    defer func() {
+        if state := recover(); state != nil {
+            err = log.Wrap(state.(error))
+        }
+    }()
+
+    // TODO(dustin): !! Add tests
+
+    typeSize := tagType.Size()
+
+    if len(rawBytes)%typeSize != 0 {
+        log.Panicf("byte-count (%d) does not align for [%s] type with a size of (%d) bytes", len(rawBytes), TypeNames[tagType], typeSize)
+    }
+
+    // unitCount is the calculated unit-count. This should equal the original
+    // value from the tag (pre-resolution).
+    unitCount := uint32(len(rawBytes) / typeSize)
+
+    // Truncate the items if it's not bytes or a string and we just want the first.
+
+    valueSuffix := ""
+    if justFirst == true && unitCount > 1 && tagType != TypeByte && tagType != TypeAscii && tagType != TypeAsciiNoNul {
+        unitCount = 1
+        valueSuffix = "..."
+    }
+
+    if tagType == TypeByte {
+        items, err := parser.ParseBytes(rawBytes, unitCount)
+        log.PanicIf(err)
+
+        return DumpBytesToString(items), nil
+    } else if tagType == TypeAscii {
+        phrase, err := parser.ParseAscii(rawBytes, unitCount)
+        log.PanicIf(err)
+
+        return phrase, nil
+    } else if tagType == TypeAsciiNoNul {
+        phrase, err := parser.ParseAsciiNoNul(rawBytes, unitCount)
+        log.PanicIf(err)
+
+        return phrase, nil
+    } else if tagType == TypeShort {
+        items, err := parser.ParseShorts(rawBytes, unitCount, byteOrder)
+        log.PanicIf(err)
+
+        if len(items) > 0 {
+            if justFirst == true {
+                return fmt.Sprintf("%v%s", items[0], valueSuffix), nil
+            } else {
+                return fmt.Sprintf("%v", items), nil
+            }
+        } else {
+            return "", nil
+        }
+    } else if tagType == TypeLong {
+        items, err := parser.ParseLongs(rawBytes, unitCount, byteOrder)
+        log.PanicIf(err)
+
+        if len(items) > 0 {
+            if justFirst == true {
+                return fmt.Sprintf("%v%s", items[0], valueSuffix), nil
+            } else {
+                return fmt.Sprintf("%v", items), nil
+            }
+        } else {
+            return "", nil
+        }
+    } else if tagType == TypeRational {
+        items, err := parser.ParseRationals(rawBytes, unitCount, byteOrder)
+        log.PanicIf(err)
+
+        if len(items) > 0 {
+            parts := make([]string, len(items))
+            for i, r := range items {
+                parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator)
+            }
+
+            if justFirst == true {
+                return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil
+            } else {
+                return fmt.Sprintf("%v", parts), nil
+            }
+        } else {
+            return "", nil
+        }
+    } else if tagType == TypeSignedLong {
+        items, err := parser.ParseSignedLongs(rawBytes, unitCount, byteOrder)
+        log.PanicIf(err)
+
+        if len(items) > 0 {
+            if justFirst == true {
+                return fmt.Sprintf("%v%s", items[0], valueSuffix), nil
+            } else {
+                return fmt.Sprintf("%v", items), nil
+            }
+        } else {
+            return "", nil
+        }
+    } else if tagType == TypeSignedRational {
+        items, err := parser.ParseSignedRationals(rawBytes, unitCount, byteOrder)
+        log.PanicIf(err)
+
+        parts := make([]string, len(items))
+        for i, r := range items {
+            parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator)
+        }
+
+        if len(items) > 0 {
+            if justFirst == true {
+                return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil
+            } else {
+                return fmt.Sprintf("%v", parts), nil
+            }
+        } else {
+            return "", nil
+        }
+    } else {
+        // Affects only "unknown" values, in general.
+        log.Panicf("value of type [%s] can not be formatted into string", tagType.String())
+
+        // Never called.
+        return "", nil
+    }
+}
+
+func EncodeStringToBytes(tagType TagTypePrimitive, valueString string) (value interface{}, err error) {
+    defer func() {
+        if state := recover(); state != nil {
+            err = log.Wrap(state.(error))
+        }
+    }()
+
+    if tagType == TypeUndefined {
+        // TODO(dustin): Circle back to this.
+        log.Panicf("undefined-type values are not supported")
+    }
+
+    if tagType == TypeByte {
+        return []byte(valueString), nil
+    } else if tagType == TypeAscii || tagType == TypeAsciiNoNul {
+        // Whether or not we're putting an NUL on the end is only relevant for
+        // byte-level encoding. This function really just supports a user
+        // interface.
+
+        return valueString, nil
+    } else if tagType == TypeShort {
+        n, err := strconv.ParseUint(valueString, 10, 16)
+        log.PanicIf(err)
+
+        return uint16(n), nil
+    } else if tagType == TypeLong {
+        n, err := strconv.ParseUint(valueString, 10, 32)
+        log.PanicIf(err)
+
+        return uint32(n), nil
+    } else if tagType == TypeRational {
+        parts := strings.SplitN(valueString, "/", 2)
+
+        numerator, err := strconv.ParseUint(parts[0], 10, 32)
+        log.PanicIf(err)
+
+        denominator, err := strconv.ParseUint(parts[1], 10, 32)
+        log.PanicIf(err)
+
+        return Rational{
+            Numerator:   uint32(numerator),
+            Denominator: uint32(denominator),
+        }, nil
+    } else if tagType == TypeSignedLong {
+        n, err := strconv.ParseInt(valueString, 10, 32)
+        log.PanicIf(err)
+
+        return int32(n), nil
+    } else if tagType == TypeSignedRational {
+        parts := strings.SplitN(valueString, "/", 2)
+
+        numerator, err := strconv.ParseInt(parts[0], 10, 32)
+        log.PanicIf(err)
+
+        denominator, err := strconv.ParseInt(parts[1], 10, 32)
+        log.PanicIf(err)
+
+        return SignedRational{
+            Numerator:   int32(numerator),
+            Denominator: int32(denominator),
+        }, nil
+    }
+
+    log.Panicf("from-string encoding for type not supported; this shouldn't happen: [%s]", tagType.String())
+    return nil, nil
+}
+
+func init() {
+    for typeId, typeName := range TypeNames {
+        TypeNamesR[typeName] = typeId
+    }
+}
diff --git a/v2/type_encode.go b/v2/type_encode.go
new file mode 100644
index 0000000..f2c2e91
--- /dev/null
+++ b/v2/type_encode.go
@@ -0,0 +1,262 @@
+package exif
+
+import (
+    "bytes"
+    "reflect"
+
+    "encoding/binary"
+
+    "github.com/dsoprea/go-logging"
+)
+
+var (
+    typeEncodeLogger = log.NewLogger("exif.type_encode")
+)
+
+// EncodedData encapsulates the compound output of an encoding operation.
+type EncodedData struct {
+    Type    TagTypePrimitive
+    Encoded []byte
+
+    // TODO(dustin): Is this really necessary? We might have this just to correlate to the incoming stream format (raw bytes and a unit-count both for incoming and outgoing).
+    UnitCount uint32
+}
+
+type ValueEncoder struct {
+    byteOrder binary.ByteOrder
+}
+
+func NewValueEncoder(byteOrder binary.ByteOrder) *ValueEncoder {
+    return &ValueEncoder{
+        byteOrder: byteOrder,
+    }
+}
+
+func (ve *ValueEncoder) encodeBytes(value []uint8) (ed EncodedData, err error) {
+    ed.Type = TypeByte
+    ed.Encoded = []byte(value)
+    ed.UnitCount = uint32(len(value))
+
+    return ed, nil
+}
+
+func (ve *ValueEncoder) encodeAscii(value string) (ed EncodedData, err error) {
+    ed.Type = TypeAscii
+
+    ed.Encoded = []byte(value)
+    ed.Encoded = append(ed.Encoded, 0)
+
+    ed.UnitCount = uint32(len(ed.Encoded))
+
+    return ed, nil
+}
+
+// encodeAsciiNoNul returns a string encoded as a byte-string without a trailing
+// NUL byte.
+//
+// Note that:
+//
+// 1. This type can not be automatically encoded using `Encode()`. The default
+//    mode is to encode *with* a trailing NUL byte using `encodeAscii`. Only
+//    certain undefined-type tags using an unterminated ASCII string and these
+//    are exceptional in nature.
+//
+// 2. The presence of this method allows us to completely test the complimentary
+//    no-nul parser.
+//
+func (ve *ValueEncoder) encodeAsciiNoNul(value string) (ed EncodedData, err error) {
+    ed.Type = TypeAsciiNoNul
+    ed.Encoded = []byte(value)
+    ed.UnitCount = uint32(len(ed.Encoded))
+
+    return ed, nil
+}
+
+func (ve *ValueEncoder) encodeShorts(value []uint16) (ed EncodedData, err error) {
+    defer func() {
+        if state := recover(); state != nil {
+            err = log.Wrap(state.(error))
+        }
+    }()
+
+    ed.UnitCount = uint32(len(value))
+    ed.Encoded = make([]byte, ed.UnitCount*2)
+
+    for i := uint32(0); i < ed.UnitCount; i++ {
+        ve.byteOrder.PutUint16(ed.Encoded[i*2:(i+1)*2], value[i])
+    }
+
+    ed.Type = TypeShort
+
+    return ed, nil
+}
+
+func (ve *ValueEncoder) encodeLongs(value []uint32) (ed EncodedData, err error) {
+    defer func() {
+        if state := recover(); state != nil {
+            err = log.Wrap(state.(error))
+        }
+    }()
+
+    ed.UnitCount = uint32(len(value))
+    ed.Encoded = make([]byte, ed.UnitCount*4)
+
+    for i := uint32(0); i < ed.UnitCount; i++ {
+        ve.byteOrder.PutUint32(ed.Encoded[i*4:(i+1)*4], value[i])
+    }
+
+    ed.Type = TypeLong
+
+    return ed, nil
+}
+
+func (ve *ValueEncoder) encodeRationals(value []Rational) (ed EncodedData, err error) {
+    defer func() {
+        if state := recover(); state != nil {
+            err = log.Wrap(state.(error))
+        }
+    }()
+
+    ed.UnitCount = uint32(len(value))
+    ed.Encoded = make([]byte, ed.UnitCount*8)
+
+    for i := uint32(0); i < ed.UnitCount; i++ {
+        ve.byteOrder.PutUint32(ed.Encoded[i*8+0:i*8+4], value[i].Numerator)
+        ve.byteOrder.PutUint32(ed.Encoded[i*8+4:i*8+8], value[i].Denominator)
+    }
+
+    ed.Type = TypeRational
+
+    return ed, nil
+}
+
+func (ve *ValueEncoder) encodeSignedLongs(value []int32) (ed EncodedData, err error) {
+    defer func() {
+        if state := recover(); state != nil {
+            err = log.Wrap(state.(error))
+        }
+    }()
+
+    ed.UnitCount = uint32(len(value))
+
+    b := bytes.NewBuffer(make([]byte, 0, 8*ed.UnitCount))
+
+    for i := uint32(0); i < ed.UnitCount; i++ {
+        err := binary.Write(b, ve.byteOrder, value[i])
+        log.PanicIf(err)
+    }
+
+    ed.Type = TypeSignedLong
+    ed.Encoded = b.Bytes()
+
+    return ed, nil
+}
+
+func (ve *ValueEncoder) encodeSignedRationals(value []SignedRational) (ed EncodedData, err error) {
+    defer func() {
+        if state := recover(); state != nil {
+            err = log.Wrap(state.(error))
+        }
+    }()
+
+    ed.UnitCount = uint32(len(value))
+
+    b := bytes.NewBuffer(make([]byte, 0, 8*ed.UnitCount))
+
+    for i := uint32(0); i < ed.UnitCount; i++ {
+        err := binary.Write(b, ve.byteOrder, value[i].Numerator)
+        log.PanicIf(err)
+
+        err = binary.Write(b, ve.byteOrder, value[i].Denominator)
+        log.PanicIf(err)
+    }
+
+    ed.Type = TypeSignedRational
+    ed.Encoded = b.Bytes()
+
+    return ed, nil
+}
+
+// Encode returns bytes for the given value, infering type from the actual
+// value. This does not support `TypeAsciiNoNull` (all strings are encoded as
+// `TypeAscii`).
+func (ve *ValueEncoder) Encode(value interface{}) (ed EncodedData, err error) {
+    defer func() {
+        if state := recover(); state != nil {
+            err = log.Wrap(state.(error))
+        }
+    }()
+
+    // TODO(dustin): This is redundant with EncodeWithType. Refactor one to use the other.
+
+    switch value.(type) {
+    case []byte:
+        ed, err = ve.encodeBytes(value.([]byte))
+        log.PanicIf(err)
+    case string:
+        ed, err = ve.encodeAscii(value.(string))
+        log.PanicIf(err)
+    case []uint16:
+        ed, err = ve.encodeShorts(value.([]uint16))
+        log.PanicIf(err)
+    case []uint32:
+        ed, err = ve.encodeLongs(value.([]uint32))
+        log.PanicIf(err)
+    case []Rational:
+        ed, err = ve.encodeRationals(value.([]Rational))
+        log.PanicIf(err)
+    case []int32:
+        ed, err = ve.encodeSignedLongs(value.([]int32))
+        log.PanicIf(err)
+    case []SignedRational:
+        ed, err = ve.encodeSignedRationals(value.([]SignedRational))
+        log.PanicIf(err)
+    default:
+        log.Panicf("value not encodable: [%s] [%v]", reflect.TypeOf(value), value)
+    }
+
+    return ed, nil
+}
+
+// EncodeWithType returns bytes for the given value, using the given `TagType`
+// value to determine how to encode. This supports `TypeAsciiNoNul`.
+func (ve *ValueEncoder) EncodeWithType(tt TagType, value interface{}) (ed EncodedData, err error) {
+    defer func() {
+        if state := recover(); state != nil {
+            err = log.Wrap(state.(error))
+        }
+    }()
+
+    // TODO(dustin): This is redundant with Encode. Refactor one to use the other.
+
+    switch tt.Type() {
+    case TypeByte:
+        ed, err = ve.encodeBytes(value.([]byte))
+        log.PanicIf(err)
+    case TypeAscii:
+        ed, err = ve.encodeAscii(value.(string))
+        log.PanicIf(err)
+    case TypeAsciiNoNul:
+        ed, err = ve.encodeAsciiNoNul(value.(string))
+        log.PanicIf(err)
+    case TypeShort:
+        ed, err = ve.encodeShorts(value.([]uint16))
+        log.PanicIf(err)
+    case TypeLong:
+        ed, err = ve.encodeLongs(value.([]uint32))
+        log.PanicIf(err)
+    case TypeRational:
+        ed, err = ve.encodeRationals(value.([]Rational))
+        log.PanicIf(err)
+    case TypeSignedLong:
+        ed, err = ve.encodeSignedLongs(value.([]int32))
+        log.PanicIf(err)
+    case TypeSignedRational:
+        ed, err = ve.encodeSignedRationals(value.([]SignedRational))
+        log.PanicIf(err)
+    default:
+        log.Panicf("value not encodable (with type): %v [%v]", tt, value)
+    }
+
+    return ed, nil
+}
diff --git a/v2/type_encode_test.go b/v2/type_encode_test.go
new file mode 100644
index 0000000..99aed48
--- /dev/null
+++ b/v2/type_encode_test.go
@@ -0,0 +1,566 @@
+package exif
+
+import (
+    "testing"
+    "reflect"
+
+    "github.com/dsoprea/go-logging"
+)
+
+func TestByteCycle(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := []byte("original text")
+
+    ed, err := ve.encodeBytes(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeByte {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte(original)
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 13 {
+        t.Fatalf("Unit-count not correct.")
+    }
+
+    tt := NewTagType(ed.Type, byteOrder)
+    recovered, err := tt.ParseBytes(ed.Encoded, ed.UnitCount)
+
+    if reflect.DeepEqual(recovered, original) != true {
+        t.Fatalf("Value not recovered correctly.")
+    }
+}
+
+func TestAsciiCycle(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := "original text"
+
+    ed, err := ve.encodeAscii(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeAscii {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte(original)
+    expected = append(expected, 0)
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 14 {
+        t.Fatalf("Unit-count not correct.")
+    }
+
+    // Check that the string was recovered correctly and with the trailing NUL
+    // character autostripped.
+
+    tt := NewTagType(TypeAscii, byteOrder)
+    recovered, err := tt.ParseAscii(ed.Encoded, ed.UnitCount)
+
+    if reflect.DeepEqual(recovered, original) != true {
+        t.Fatalf("Value not recovered correctly.")
+    }
+}
+
+func TestAsciiNoNulCycle(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := "original text"
+
+    ed, err := ve.encodeAsciiNoNul(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeAsciiNoNul {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte(original)
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 13 {
+        t.Fatalf("Unit-count not correct.")
+    }
+
+    // Check that the string was recovered correctly and with the trailing NUL
+    // character ignored (because not expected in the context of that type).
+
+    tt := NewTagType(TypeAsciiNoNul, byteOrder)
+    recovered, err := tt.ParseAsciiNoNul(ed.Encoded, ed.UnitCount)
+
+    if reflect.DeepEqual(recovered, string(expected)) != true {
+        t.Fatalf("Value not recovered correctly.")
+    }
+}
+
+func TestShortCycle(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := []uint16 { 0x11, 0x22, 0x33, 0x44, 0x55 }
+
+    ed, err := ve.encodeShorts(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeShort {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte {
+        0x00, 0x11,
+        0x00, 0x22,
+        0x00, 0x33,
+        0x00, 0x44,
+        0x00, 0x55,
+    }
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 5 {
+        t.Fatalf("Unit-count not correct.")
+    }
+
+    tt := NewTagType(ed.Type, byteOrder)
+    recovered, err := tt.ParseShorts(ed.Encoded, ed.UnitCount)
+
+    if reflect.DeepEqual(recovered, original) != true {
+        t.Fatalf("Value not recovered correctly.")
+    }
+}
+
+func TestLongCycle(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := []uint32 { 0x11, 0x22, 0x33, 0x44, 0x55 }
+
+    ed, err := ve.encodeLongs(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeLong {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte {
+        0x00, 0x00, 0x00, 0x11,
+        0x00, 0x00, 0x00, 0x22,
+        0x00, 0x00, 0x00, 0x33,
+        0x00, 0x00, 0x00, 0x44,
+        0x00, 0x00, 0x00, 0x55,
+    }
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 5 {
+        t.Fatalf("Unit-count not correct.")
+    }
+
+    tt := NewTagType(ed.Type, byteOrder)
+    recovered, err := tt.ParseLongs(ed.Encoded, ed.UnitCount)
+
+    if reflect.DeepEqual(recovered, original) != true {
+        t.Fatalf("Value not recovered correctly.")
+    }
+}
+
+func TestRationalCycle(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := []Rational {
+        Rational{
+            Numerator: 0x11,
+            Denominator: 0x22,
+        },
+        Rational{
+            Numerator: 0x33,
+            Denominator: 0x44,
+        },
+        Rational{
+            Numerator: 0x55,
+            Denominator: 0x66,
+        },
+        Rational{
+            Numerator: 0x77,
+            Denominator: 0x88,
+        },
+        Rational{
+            Numerator: 0x99,
+            Denominator: 0x00,
+        },
+    }
+
+    ed, err := ve.encodeRationals(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeRational {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte {
+        0x00, 0x00, 0x00, 0x11,
+        0x00, 0x00, 0x00, 0x22,
+        0x00, 0x00, 0x00, 0x33,
+        0x00, 0x00, 0x00, 0x44,
+        0x00, 0x00, 0x00, 0x55,
+        0x00, 0x00, 0x00, 0x66,
+        0x00, 0x00, 0x00, 0x77,
+        0x00, 0x00, 0x00, 0x88,
+        0x00, 0x00, 0x00, 0x99,
+        0x00, 0x00, 0x00, 0x00,
+    }
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 5 {
+        t.Fatalf("Unit-count not correct.")
+    }
+
+    tt := NewTagType(ed.Type, byteOrder)
+    recovered, err := tt.ParseRationals(ed.Encoded, ed.UnitCount)
+
+    if reflect.DeepEqual(recovered, original) != true {
+        t.Fatalf("Value not recovered correctly.")
+    }
+}
+
+func TestSignedLongCycle(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := []int32 { 0x11, 0x22, 0x33, 0x44, 0x55 }
+
+    ed, err := ve.encodeSignedLongs(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeSignedLong {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte {
+        0x00, 0x00, 0x00, 0x11,
+        0x00, 0x00, 0x00, 0x22,
+        0x00, 0x00, 0x00, 0x33,
+        0x00, 0x00, 0x00, 0x44,
+        0x00, 0x00, 0x00, 0x55,
+    }
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 5 {
+        t.Fatalf("Unit-count not correct.")
+    }
+
+    tt := NewTagType(ed.Type, byteOrder)
+    recovered, err := tt.ParseSignedLongs(ed.Encoded, ed.UnitCount)
+
+    if reflect.DeepEqual(recovered, original) != true {
+        t.Fatalf("Value not recovered correctly.")
+    }
+}
+
+func TestSignedRationalCycle(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := []SignedRational {
+        SignedRational{
+            Numerator: 0x11,
+            Denominator: 0x22,
+        },
+        SignedRational{
+            Numerator: 0x33,
+            Denominator: 0x44,
+        },
+        SignedRational{
+            Numerator: 0x55,
+            Denominator: 0x66,
+        },
+        SignedRational{
+            Numerator: 0x77,
+            Denominator: 0x88,
+        },
+        SignedRational{
+            Numerator: 0x99,
+            Denominator: 0x00,
+        },
+    }
+
+    ed, err := ve.encodeSignedRationals(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeSignedRational {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte {
+        0x00, 0x00, 0x00, 0x11,
+        0x00, 0x00, 0x00, 0x22,
+        0x00, 0x00, 0x00, 0x33,
+        0x00, 0x00, 0x00, 0x44,
+        0x00, 0x00, 0x00, 0x55,
+        0x00, 0x00, 0x00, 0x66,
+        0x00, 0x00, 0x00, 0x77,
+        0x00, 0x00, 0x00, 0x88,
+        0x00, 0x00, 0x00, 0x99,
+        0x00, 0x00, 0x00, 0x00,
+    }
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 5 {
+        t.Fatalf("Unit-count not correct.")
+    }
+
+    tt := NewTagType(ed.Type, byteOrder)
+    recovered, err := tt.ParseSignedRationals(ed.Encoded, ed.UnitCount)
+
+    if reflect.DeepEqual(recovered, original) != true {
+        t.Fatalf("Value not recovered correctly.")
+    }
+}
+
+func TestEncode_Byte(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := []byte("original text")
+
+    ed, err := ve.Encode(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeByte {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte(original)
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 13 {
+        t.Fatalf("Unit-count not correct.")
+    }
+}
+
+func TestEncode_Ascii(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := "original text"
+
+    ed, err := ve.Encode(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeAscii {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte(original)
+    expected = append(expected, 0)
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 14 {
+        t.Fatalf("Unit-count not correct.")
+    }
+}
+
+func TestEncode_Short(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := []uint16 { 0x11, 0x22, 0x33, 0x44, 0x55 }
+
+    ed, err := ve.Encode(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeShort {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte {
+        0x00, 0x11,
+        0x00, 0x22,
+        0x00, 0x33,
+        0x00, 0x44,
+        0x00, 0x55,
+    }
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 5 {
+        t.Fatalf("Unit-count not correct.")
+    }
+}
+
+func TestEncode_Long(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := []uint32 { 0x11, 0x22, 0x33, 0x44, 0x55 }
+
+    ed, err := ve.Encode(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeLong {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte {
+        0x00, 0x00, 0x00, 0x11,
+        0x00, 0x00, 0x00, 0x22,
+        0x00, 0x00, 0x00, 0x33,
+        0x00, 0x00, 0x00, 0x44,
+        0x00, 0x00, 0x00, 0x55,
+    }
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 5 {
+        t.Fatalf("Unit-count not correct.")
+    }
+}
+
+func TestEncode_Rational(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := []Rational {
+        Rational{
+            Numerator: 0x11,
+            Denominator: 0x22,
+        },
+        Rational{
+            Numerator: 0x33,
+            Denominator: 0x44,
+        },
+        Rational{
+            Numerator: 0x55,
+            Denominator: 0x66,
+        },
+        Rational{
+            Numerator: 0x77,
+            Denominator: 0x88,
+        },
+        Rational{
+            Numerator: 0x99,
+            Denominator: 0x00,
+        },
+    }
+
+    ed, err := ve.Encode(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeRational {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte {
+        0x00, 0x00, 0x00, 0x11,
+        0x00, 0x00, 0x00, 0x22,
+        0x00, 0x00, 0x00, 0x33,
+        0x00, 0x00, 0x00, 0x44,
+        0x00, 0x00, 0x00, 0x55,
+        0x00, 0x00, 0x00, 0x66,
+        0x00, 0x00, 0x00, 0x77,
+        0x00, 0x00, 0x00, 0x88,
+        0x00, 0x00, 0x00, 0x99,
+        0x00, 0x00, 0x00, 0x00,
+    }
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 5 {
+        t.Fatalf("Unit-count not correct.")
+    }
+}
+
+func TestEncode_SignedLong(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := []int32 { 0x11, 0x22, 0x33, 0x44, 0x55 }
+
+    ed, err := ve.Encode(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeSignedLong {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte {
+        0x00, 0x00, 0x00, 0x11,
+        0x00, 0x00, 0x00, 0x22,
+        0x00, 0x00, 0x00, 0x33,
+        0x00, 0x00, 0x00, 0x44,
+        0x00, 0x00, 0x00, 0x55,
+    }
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 5 {
+        t.Fatalf("Unit-count not correct.")
+    }
+}
+
+func TestEncode_SignedRational(t *testing.T) {
+    byteOrder := TestDefaultByteOrder
+    ve := NewValueEncoder(byteOrder)
+
+    original := []SignedRational {
+        SignedRational{
+            Numerator: 0x11,
+            Denominator: 0x22,
+        },
+        SignedRational{
+            Numerator: 0x33,
+            Denominator: 0x44,
+        },
+        SignedRational{
+            Numerator: 0x55,
+            Denominator: 0x66,
+        },
+        SignedRational{
+            Numerator: 0x77,
+            Denominator: 0x88,
+        },
+        SignedRational{
+            Numerator: 0x99,
+            Denominator: 0x00,
+        },
+    }
+
+    ed, err := ve.Encode(original)
+    log.PanicIf(err)
+
+    if ed.Type != TypeSignedRational {
+        t.Fatalf("IFD type not expected.")
+    }
+
+    expected := []byte {
+        0x00, 0x00, 0x00, 0x11,
+        0x00, 0x00, 0x00, 0x22,
+        0x00, 0x00, 0x00, 0x33,
+        0x00, 0x00, 0x00, 0x44,
+        0x00, 0x00, 0x00, 0x55,
+        0x00, 0x00, 0x00, 0x66,
+        0x00, 0x00, 0x00, 0x77,
+        0x00, 0x00, 0x00, 0x88,
+        0x00, 0x00, 0x00, 0x99,
+        0x00, 0x00, 0x00, 0x00,
+    }
+
+    if reflect.DeepEqual(ed.Encoded, expected) != true {
+        t.Fatalf("Data not encoded correctly.")
+    } else if ed.UnitCount != 5 {
+        t.Fatalf("Unit-count not correct.")
+    }
+}
diff --git a/v2/type_test.go b/v2/type_test.go
new file mode 100644
index 0000000..2327064
--- /dev/null
+++ b/v2/type_test.go
@@ -0,0 +1,297 @@
+package exif
+
+import (
+    "testing"
+    "bytes"
+    "fmt"
+    "reflect"
+
+    "github.com/dsoprea/go-logging"
+)
+
+func TestTagType_EncodeDecode_Byte(t *testing.T) {
+    tt := NewTagType(TypeByte, TestDefaultByteOrder)
+
+    data := []byte { 0x11, 0x22, 0x33, 0x44, 0x55 }
+
+    encoded, err := tt.Encode(data)
+    log.PanicIf(err)
+
+    if bytes.Compare(encoded, data) != 0 {
+        t.Fatalf("Data not encoded correctly.")
+    }
+
+    restored, err := tt.ParseBytes(encoded, uint32(len(data)))
+    log.PanicIf(err)
+
+    if bytes.Compare(restored, data) != 0 {
+        t.Fatalf("Data not decoded correctly.")
+    }
+}
+
+func TestTagType_EncodeDecode_Ascii(t *testing.T) {
+    tt := NewTagType(TypeAscii, TestDefaultByteOrder)
+
+    data := "hello"
+
+    encoded, err := tt.Encode(data)
+    log.PanicIf(err)
+
+    if string(encoded) != fmt.Sprintf("%s\000", data) {
+        t.Fatalf("Data not encoded correctly.")
+    }
+
+    restored, err := tt.ParseAscii(encoded, uint32(len(data)))
+    log.PanicIf(err)
+
+    if restored != data {
+        t.Fatalf("Data not decoded correctly.")
+    }
+}
+
+func TestTagType_EncodeDecode_Shorts(t *testing.T) {
+    tt := NewTagType(TypeShort, TestDefaultByteOrder)
+
+    data := []uint16 { 0x11, 0x22, 0x33 }
+
+    encoded, err := tt.Encode(data)
+    log.PanicIf(err)
+
+    if bytes.Compare(encoded, []byte { 0x00, 0x11, 0x00, 0x22, 0x00, 0x33 }) != 0 {
+        t.Fatalf("Data not encoded correctly.")
+    }
+
+    restored, err := tt.ParseShorts(encoded, uint32(len(data)))
+    log.PanicIf(err)
+
+    if reflect.DeepEqual(restored, data) != true {
+        t.Fatalf("Data not decoded correctly.")
+    }
+}
+
+func TestTagType_EncodeDecode_Long(t *testing.T) {
+    tt := NewTagType(TypeLong, TestDefaultByteOrder)
+
+    data := []uint32 { 0x11, 0x22, 0x33 }
+
+    encoded, err := tt.Encode(data)
+    log.PanicIf(err)
+
+    if bytes.Compare(encoded, []byte { 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x33 }) != 0 {
+        t.Fatalf("Data not encoded correctly.")
+    }
+
+    restored, err := tt.ParseLongs(encoded, uint32(len(data)))
+    log.PanicIf(err)
+
+    if reflect.DeepEqual(restored, data) != true {
+        t.Fatalf("Data not decoded correctly.")
+    }
+}
+
+func TestTagType_EncodeDecode_Rational(t *testing.T) {
+    tt := NewTagType(TypeRational, TestDefaultByteOrder)
+
+    data := []Rational {
+        Rational{ Numerator: 0x11, Denominator: 0x22 },
+        Rational{ Numerator: 0x33, Denominator: 0x44 },
+    }
+
+    encoded, err := tt.Encode(data)
+    log.PanicIf(err)
+
+    if bytes.Compare(encoded, []byte { 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x44 }) != 0 {
+        t.Fatalf("Data not encoded correctly.")
+    }
+
+    restored, err := tt.ParseRationals(encoded, uint32(len(data)))
+    log.PanicIf(err)
+
+    if reflect.DeepEqual(restored, data) != true {
+        t.Fatalf("Data not decoded correctly.")
+    }
+}
+
+func TestTagType_EncodeDecode_SignedLong(t *testing.T) {
+    tt := NewTagType(TypeSignedLong, TestDefaultByteOrder)
+
+    data := []int32 { 0x11, 0x22, 0x33 }
+
+    encoded, err := tt.Encode(data)
+    log.PanicIf(err)
+
+    if bytes.Compare(encoded, []byte { 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x33 }) != 0 {
+        t.Fatalf("Data not encoded correctly.")
+    }
+
+    restored, err := tt.ParseSignedLongs(encoded, uint32(len(data)))
+    log.PanicIf(err)
+
+    if reflect.DeepEqual(restored, data) != true {
+        t.Fatalf("Data not decoded correctly.")
+    }
+}
+
+func TestTagType_EncodeDecode_SignedRational(t *testing.T) {
+    tt := NewTagType(TypeSignedRational, TestDefaultByteOrder)
+
+    data := []SignedRational {
+        SignedRational{ Numerator: 0x11, Denominator: 0x22 },
+        SignedRational{ Numerator: 0x33, Denominator: 0x44 },
+    }
+
+    encoded, err := tt.Encode(data)
+    log.PanicIf(err)
+
+    if bytes.Compare(encoded, []byte { 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x44 }) != 0 {
+        t.Fatalf("Data not encoded correctly.")
+    }
+
+    restored, err := tt.ParseSignedRationals(encoded, uint32(len(data)))
+    log.PanicIf(err)
+
+    if reflect.DeepEqual(restored, data) != true {
+        t.Fatalf("Data not decoded correctly.")
+    }
+}
+
+func TestTagType_EncodeDecode_AsciiNoNul(t *testing.T) {
+    tt := NewTagType(TypeAsciiNoNul, TestDefaultByteOrder)
+
+    data := "hello"
+
+    encoded, err := tt.Encode(data)
+    log.PanicIf(err)
+
+    if string(encoded) != data {
+        t.Fatalf("Data not encoded correctly.")
+    }
+
+    restored, err := tt.ParseAsciiNoNul(encoded, uint32(len(data)))
+    log.PanicIf(err)
+
+    if restored != data {
+        t.Fatalf("Data not decoded correctly.")
+    }
+}
+
+// TODO(dustin): Add tests for TypeUndefined.
+
+func TestTagType_FromString_Undefined(t *testing.T) {
+    defer func() {
+        if state := recover(); state != nil {
+            err := log.Wrap(state.(error))
+            log.PrintErrorf(err, "Test failure.")
+
+            log.Panic(err)
+        }
+    }()
+
+    tt := NewTagType(TypeUndefined, TestDefaultByteOrder)
+
+    _, err := tt.FromString("")
+    if err == nil {
+        t.Fatalf("no error for undefined-type")
+    } else if err.Error() != "undefined-type values are not supported" {
+        fmt.Printf("[%s]\n", err.Error())
+        log.Panic(err)
+    }
+}
+
+func TestTagType_FromString_Byte(t *testing.T) {
+    tt := NewTagType(TypeByte, TestDefaultByteOrder)
+
+    value, err := tt.FromString("abc")
+    log.PanicIf(err)
+
+    if reflect.DeepEqual(value, []byte { 'a', 'b', 'c' }) != true {
+        t.Fatalf("byte value not correct")
+    }
+}
+
+func TestTagType_FromString_Ascii(t *testing.T) {
+    tt := NewTagType(TypeAscii, TestDefaultByteOrder)
+
+    value, err := tt.FromString("abc")
+    log.PanicIf(err)
+
+    if reflect.DeepEqual(value, "abc") != true {
+        t.Fatalf("ASCII value not correct: [%s]", value)
+    }
+}
+
+func TestTagType_FromString_Short(t *testing.T) {
+    tt := NewTagType(TypeShort, TestDefaultByteOrder)
+
+    value, err := tt.FromString("55")
+    log.PanicIf(err)
+
+    if reflect.DeepEqual(value, uint16(55)) != true {
+        t.Fatalf("short value not correct")
+    }
+}
+
+func TestTagType_FromString_Long(t *testing.T) {
+    tt := NewTagType(TypeLong, TestDefaultByteOrder)
+
+    value, err := tt.FromString("66000")
+    log.PanicIf(err)
+
+    if reflect.DeepEqual(value, uint32(66000)) != true {
+        t.Fatalf("long value not correct")
+    }
+}
+
+func TestTagType_FromString_Rational(t *testing.T) {
+    tt := NewTagType(TypeRational, TestDefaultByteOrder)
+
+    value, err := tt.FromString("12/34")
+    log.PanicIf(err)
+
+    expected := Rational{
+        Numerator: 12,
+        Denominator: 34,
+    }
+
+    if reflect.DeepEqual(value, expected) != true {
+        t.Fatalf("rational value not correct")
+    }
+}
+
+func TestTagType_FromString_SignedLong(t *testing.T) {
+    tt := NewTagType(TypeSignedLong, TestDefaultByteOrder)
+
+    value, err := tt.FromString("-66000")
+    log.PanicIf(err)
+
+    if reflect.DeepEqual(value, int32(-66000)) != true {
+        t.Fatalf("signed-long value not correct")
+    }
+}
+
+func TestTagType_FromString_SignedRational(t *testing.T) {
+    tt := NewTagType(TypeSignedRational, TestDefaultByteOrder)
+
+    value, err := tt.FromString("-12/34")
+    log.PanicIf(err)
+
+    expected := SignedRational{
+        Numerator: -12,
+        Denominator: 34,
+    }
+
+    if reflect.DeepEqual(value, expected) != true {
+        t.Fatalf("signd-rational value not correct")
+    }
+}
+
+func TestTagType_FromString_AsciiNoNul(t *testing.T) {
+    tt := NewTagType(TypeAsciiNoNul, TestDefaultByteOrder)
+
+    value, err := tt.FromString("abc")
+    log.PanicIf(err)
+
+    if reflect.DeepEqual(value, "abc") != true {
+        t.Fatalf("ASCII-no-nul value not correct")
+    }
+}
diff --git a/v2/utility.go b/v2/utility.go
new file mode 100644
index 0000000..d7f3006
--- /dev/null
+++ b/v2/utility.go
@@ -0,0 +1,222 @@
+package exif
+
+import (
+    "bytes"
+    "fmt"
+    "strconv"
+    "strings"
+    "time"
+
+    "github.com/dsoprea/go-logging"
+)
+
+func DumpBytes(data []byte) {
+    fmt.Printf("DUMP: ")
+    for _, x := range data {
+        fmt.Printf("%02x ", x)
+    }
+
+    fmt.Printf("\n")
+}
+
+func DumpBytesClause(data []byte) {
+    fmt.Printf("DUMP: ")
+
+    fmt.Printf("[]byte { ")
+
+    for i, x := range data {
+        fmt.Printf("0x%02x", x)
+
+        if i < len(data)-1 {
+            fmt.Printf(", ")
+        }
+    }
+
+    fmt.Printf(" }\n")
+}
+
+func DumpBytesToString(data []byte) string {
+    b := new(bytes.Buffer)
+
+    for i, x := range data {
+        _, err := b.WriteString(fmt.Sprintf("%02x", x))
+        log.PanicIf(err)
+
+        if i < len(data)-1 {
+            _, err := b.WriteRune(' ')
+            log.PanicIf(err)
+        }
+    }
+
+    return b.String()
+}
+
+func DumpBytesClauseToString(data []byte) string {
+    b := new(bytes.Buffer)
+
+    for i, x := range data {
+        _, err := b.WriteString(fmt.Sprintf("0x%02x", x))
+        log.PanicIf(err)
+
+        if i < len(data)-1 {
+            _, err := b.WriteString(", ")
+            log.PanicIf(err)
+        }
+    }
+
+    return b.String()
+}
+
+// ParseExifFullTimestamp parses dates like "2018:11:30 13:01:49" into a UTC
+// `time.Time` struct.
+func ParseExifFullTimestamp(fullTimestampPhrase string) (timestamp time.Time, err error) {
+    defer func() {
+        if state := recover(); state != nil {
+            err = log.Wrap(state.(error))
+        }
+    }()
+
+    parts := strings.Split(fullTimestampPhrase, " ")
+    datestampValue, timestampValue := parts[0], parts[1]
+
+    dateParts := strings.Split(datestampValue, ":")
+
+    year, err := strconv.ParseUint(dateParts[0], 10, 16)
+    if err != nil {
+        log.Panicf("could not parse year")
+    }
+
+    month, err := strconv.ParseUint(dateParts[1], 10, 8)
+    if err != nil {
+        log.Panicf("could not parse month")
+    }
+
+    day, err := strconv.ParseUint(dateParts[2], 10, 8)
+    if err != nil {
+        log.Panicf("could not parse day")
+    }
+
+    timeParts := strings.Split(timestampValue, ":")
+
+    hour, err := strconv.ParseUint(timeParts[0], 10, 8)
+    if err != nil {
+        log.Panicf("could not parse hour")
+    }
+
+    minute, err := strconv.ParseUint(timeParts[1], 10, 8)
+    if err != nil {
+        log.Panicf("could not parse minute")
+    }
+
+    second, err := strconv.ParseUint(timeParts[2], 10, 8)
+    if err != nil {
+        log.Panicf("could not parse second")
+    }
+
+    timestamp = time.Date(int(year), time.Month(month), int(day), int(hour), int(minute), int(second), 0, time.UTC)
+    return timestamp, nil
+}
+
+// ExifFullTimestampString produces a string like "2018:11:30 13:01:49" from a
+// `time.Time` struct. It will attempt to convert to UTC first.
+func ExifFullTimestampString(t time.Time) (fullTimestampPhrase string) {
+    t = t.UTC()
+
+    return fmt.Sprintf("%04d:%02d:%02d %02d:%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
+}
+
+// ExifTag is one simple representation of a tag in a flat list of all of them.
+type ExifTag struct {
+    IfdPath string `json:"ifd_path"`
+
+    TagId   uint16 `json:"id"`
+    TagName string `json:"name"`
+
+    TagTypeId   TagTypePrimitive `json:"type_id"`
+    TagTypeName string           `json:"type_name"`
+    Value       interface{}      `json:"value"`
+    ValueBytes  []byte           `json:"value_bytes"`
+
+    ChildIfdPath string `json:"child_ifd_path"`
+}
+
+// String returns a string representation.
+func (et ExifTag) String() string {
+    return fmt.Sprintf("ExifTag<IFD-PATH=[%s] TAG-ID=(0x%02x) TAG-NAME=[%s] TAG-TYPE=[%s] VALUE=[%v] VALUE-BYTES=(%d) CHILD-IFD-PATH=[%s]", et.IfdPath, et.TagId, et.TagName, et.TagTypeName, et.Value, len(et.ValueBytes), et.ChildIfdPath)
+}
+
+// GetFlatExifData returns a simple, flat representation of all tags.
+func GetFlatExifData(exifData []byte) (exifTags []ExifTag, err error) {
+    defer func() {
+        if state := recover(); state != nil {
+            err = log.Wrap(state.(error))
+        }
+    }()
+
+    im := NewIfdMappingWithStandard()
+    ti := NewTagIndex()
+
+    _, index, err := Collect(im, ti, exifData)
+    log.PanicIf(err)
+
+    q := []*Ifd{index.RootIfd}
+
+    exifTags = make([]ExifTag, 0)
+
+    for len(q) > 0 {
+        var ifd *Ifd
+        ifd, q = q[0], q[1:]
+
+        ti := NewTagIndex()
+        for _, ite := range ifd.Entries {
+            tagName := ""
+
+            it, err := ti.Get(ifd.IfdPath, ite.TagId)
+            if err != nil {
+                // If it's a non-standard tag, just leave the name blank.
+                if log.Is(err, ErrTagNotFound) != true {
+                    log.PanicIf(err)
+                }
+            } else {
+                tagName = it.Name
+            }
+
+            value, err := ifd.TagValue(ite)
+            if err != nil {
+                if err == ErrUnhandledUnknownTypedTag {
+                    value = UnparseableUnknownTagValuePlaceholder
+                } else {
+                    log.Panic(err)
+                }
+            }
+
+            valueBytes, err := ifd.TagValueBytes(ite)
+            if err != nil && err != ErrUnhandledUnknownTypedTag {
+                log.Panic(err)
+            }
+
+            et := ExifTag{
+                IfdPath:      ifd.IfdPath,
+                TagId:        ite.TagId,
+                TagName:      tagName,
+                TagTypeId:    ite.TagType,
+                TagTypeName:  TypeNames[ite.TagType],
+                Value:        value,
+                ValueBytes:   valueBytes,
+                ChildIfdPath: ite.ChildIfdPath,
+            }
+
+            exifTags = append(exifTags, et)
+        }
+
+        for _, childIfd := range ifd.Children {
+            q = append(q, childIfd)
+        }
+
+        if ifd.NextIfd != nil {
+            q = append(q, ifd.NextIfd)
+        }
+    }
+
+    return exifTags, nil
+}
diff --git a/v2/utility_test.go b/v2/utility_test.go
new file mode 100644
index 0000000..c786e88
--- /dev/null
+++ b/v2/utility_test.go
@@ -0,0 +1,125 @@
+package exif
+
+import (
+    "io/ioutil"
+
+    "fmt"
+    "os"
+    "testing"
+    "time"
+
+    "github.com/dsoprea/go-logging"
+)
+
+func TestDumpBytes(t *testing.T) {
+    f, err := ioutil.TempFile(os.TempDir(), "utilitytest")
+    log.PanicIf(err)
+
+    defer os.Remove(f.Name())
+
+    originalStdout := os.Stdout
+    os.Stdout = f
+
+    DumpBytes([]byte{0x11, 0x22})
+
+    os.Stdout = originalStdout
+
+    _, err = f.Seek(0, 0)
+    log.PanicIf(err)
+
+    content, err := ioutil.ReadAll(f)
+    log.PanicIf(err)
+
+    if string(content) != "DUMP: 11 22 \n" {
+        t.Fatalf("content not correct: [%s]", string(content))
+    }
+}
+
+func TestDumpBytesClause(t *testing.T) {
+    f, err := ioutil.TempFile(os.TempDir(), "utilitytest")
+    log.PanicIf(err)
+
+    defer os.Remove(f.Name())
+
+    originalStdout := os.Stdout
+    os.Stdout = f
+
+    DumpBytesClause([]byte{0x11, 0x22})
+
+    os.Stdout = originalStdout
+
+    _, err = f.Seek(0, 0)
+    log.PanicIf(err)
+
+    content, err := ioutil.ReadAll(f)
+    log.PanicIf(err)
+
+    if string(content) != "DUMP: []byte { 0x11, 0x22 }\n" {
+        t.Fatalf("content not correct: [%s]", string(content))
+    }
+}
+
+func TestDumpBytesToString(t *testing.T) {
+    s := DumpBytesToString([]byte{0x12, 0x34, 0x56})
+
+    if s != "12 34 56" {
+        t.Fatalf("result not expected")
+    }
+}
+
+func TestDumpBytesClauseToString(t *testing.T) {
+    s := DumpBytesClauseToString([]byte{0x12, 0x34, 0x56})
+
+    if s != "0x12, 0x34, 0x56" {
+        t.Fatalf("result not expected")
+    }
+}
+
+func TestParseExifFullTimestamp(t *testing.T) {
+    timestamp, err := ParseExifFullTimestamp("2018:11:30 13:01:49")
+    log.PanicIf(err)
+
+    actual := timestamp.Format(time.RFC3339)
+    expected := "2018-11-30T13:01:49Z"
+
+    if actual != expected {
+        t.Fatalf("time not formatted correctly: [%s] != [%s]", actual, expected)
+    }
+}
+
+func TestExifFullTimestampString(t *testing.T) {
+    originalPhrase := "2018:11:30 13:01:49"
+
+    timestamp, err := ParseExifFullTimestamp(originalPhrase)
+    log.PanicIf(err)
+
+    restoredPhrase := ExifFullTimestampString(timestamp)
+    if restoredPhrase != originalPhrase {
+        t.Fatalf("Final phrase [%s] does not equal original phrase [%s]", restoredPhrase, originalPhrase)
+    }
+}
+
+func ExampleParseExifFullTimestamp() {
+    originalPhrase := "2018:11:30 13:01:49"
+
+    timestamp, err := ParseExifFullTimestamp(originalPhrase)
+    log.PanicIf(err)
+
+    fmt.Printf("To Go timestamp: [%s]\n", timestamp.Format(time.RFC3339))
+
+    // Output:
+    // To Go timestamp: [2018-11-30T13:01:49Z]
+}
+
+func ExampleExifFullTimestampString() {
+    originalPhrase := "2018:11:30 13:01:49"
+
+    timestamp, err := ParseExifFullTimestamp(originalPhrase)
+    log.PanicIf(err)
+
+    restoredPhrase := ExifFullTimestampString(timestamp)
+    fmt.Printf("To EXIF timestamp: [%s]\n", restoredPhrase)
+
+    // Output:
+    // To EXIF timestamp: [2018:11:30 13:01:49]
+}
diff --git a/v2/value_context.go b/v2/value_context.go
new file mode 100644
index 0000000..c5adff2
--- /dev/null
+++ b/v2/value_context.go
@@ -0,0 +1,361 @@
+package exif
+
+import (
+	"encoding/binary"
+
+	"github.com/dsoprea/go-logging"
+)
+
+var (
+	parser *Parser
+)
+
+// ValueContext describes all of the parameters required to find and extract
+// the actual tag value.
+type ValueContext struct {
+	unitCount       uint32
+	valueOffset     uint32
+	rawValueOffset  []byte
+	addressableData []byte
+
+	tagType   TagTypePrimitive
+	byteOrder binary.ByteOrder
+
+	// undefinedValueTagType is the effective type to use if this is an
+	// "undefined" value.
+	undefinedValueTagType TagTypePrimitive
+
+	ifdPath string
+	tagId   uint16
+}
+
+func newValueContext(ifdPath string, tagId uint16, unitCount, valueOffset uint32, rawValueOffset, addressableData []byte, tagType TagTypePrimitive, byteOrder binary.ByteOrder) *ValueContext {
+	return &ValueContext{
+		unitCount:       unitCount,
+		valueOffset:     valueOffset,
+		rawValueOffset:  rawValueOffset,
+		addressableData: addressableData,
+
+		tagType:   tagType,
+		byteOrder: byteOrder,
+
+		ifdPath: ifdPath,
+		tagId:   tagId,
+	}
+}
+
+func newValueContextFromTag(ite *IfdTagEntry, addressableData []byte, byteOrder binary.ByteOrder) *ValueContext {
+	return newValueContext(
+		ite.IfdPath,
+		ite.TagId,
+		ite.UnitCount,
+		ite.ValueOffset,
+		ite.RawValueOffset,
+		addressableData,
+		ite.TagType,
+		byteOrder)
+}
+
+func (vc *ValueContext) SetUnknownValueType(tagType TagTypePrimitive) {
+	vc.undefinedValueTagType = tagType
+}
+
+func (vc *ValueContext) UnitCount() uint32 {
+	return vc.unitCount
+}
+
+func (vc *ValueContext) ValueOffset() uint32 {
+	return vc.valueOffset
+}
+
+func (vc *ValueContext) RawValueOffset() []byte {
+	return vc.rawValueOffset
+}
+
+func (vc *ValueContext) AddressableData() []byte {
+	return vc.addressableData
+}
+
+// isEmbedded returns whether the value is embedded or a reference. This can't
+// be precalculated since the size is not defined for all types (namely the
+// "undefined" types).
+func (vc *ValueContext) isEmbedded() bool {
+	tagType := vc.effectiveValueType()
+
+	return (tagType.Size() * int(vc.unitCount)) <= 4
+}
+
+func (vc *ValueContext) effectiveValueType() (tagType TagTypePrimitive) {
+	if vc.tagType == TypeUndefined {
+		tagType = vc.undefinedValueTagType
+
+		if tagType == 0 {
+			log.Panicf("undefined-value type not set")
+		}
+	} else {
+		tagType = vc.tagType
+	}
+
+	return tagType
+}
+
+func (vc *ValueContext) readRawEncoded() (rawBytes []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	tagType := vc.effectiveValueType()
+
+	unitSizeRaw := uint32(tagType.Size())
+
+	if vc.isEmbedded() == true {
+		byteLength := unitSizeRaw * vc.unitCount
+		return vc.rawValueOffset[:byteLength], nil
+	} else {
+		return vc.addressableData[vc.valueOffset : vc.valueOffset+vc.unitCount*unitSizeRaw], nil
+	}
+}
+
+// Format returns a string representation for the value.
+//
+// Where the type is not ASCII, `justFirst` indicates whether to just stringify
+// the first item in the slice (or return an empty string if the slice is
+// empty).
+//
+// Since this method lacks the information to process undefined-type tags (e.g.
+// byte-order, tag-ID, IFD type), it will return an error if attempted. See
+// `Undefined()`.
+func (vc *ValueContext) Format() (value string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	rawBytes, err := vc.readRawEncoded()
+	log.PanicIf(err)
+
+	phrase, err := Format(rawBytes, vc.tagType, false, vc.byteOrder)
+	log.PanicIf(err)
+
+	return phrase, nil
+}
+
+// FormatOne is similar to `Format` but only gets and stringifies the first
+// item.
+func (vc *ValueContext) FormatFirst() (value string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	rawBytes, err := vc.readRawEncoded()
+	log.PanicIf(err)
+
+	phrase, err := Format(rawBytes, vc.tagType, true, vc.byteOrder)
+	log.PanicIf(err)
+
+	return phrase, nil
+}
+
+func (vc *ValueContext) ReadBytes() (value []byte, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	rawValue, err := vc.readRawEncoded()
+	log.PanicIf(err)
+
+	value, err = parser.ParseBytes(rawValue, vc.unitCount)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (vc *ValueContext) ReadAscii() (value string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	rawValue, err := vc.readRawEncoded()
+	log.PanicIf(err)
+
+	value, err = parser.ParseAscii(rawValue, vc.unitCount)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (vc *ValueContext) ReadAsciiNoNul() (value string, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	rawValue, err := vc.readRawEncoded()
+	log.PanicIf(err)
+
+	value, err = parser.ParseAsciiNoNul(rawValue, vc.unitCount)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (vc *ValueContext) ReadShorts() (value []uint16, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	rawValue, err := vc.readRawEncoded()
+	log.PanicIf(err)
+
+	value, err = parser.ParseShorts(rawValue, vc.unitCount, vc.byteOrder)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (vc *ValueContext) ReadLongs() (value []uint32, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	rawValue, err := vc.readRawEncoded()
+	log.PanicIf(err)
+
+	value, err = parser.ParseLongs(rawValue, vc.unitCount, vc.byteOrder)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (vc *ValueContext) ReadRationals() (value []Rational, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	rawValue, err := vc.readRawEncoded()
+	log.PanicIf(err)
+
+	value, err = parser.ParseRationals(rawValue, vc.unitCount, vc.byteOrder)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (vc *ValueContext) ReadSignedLongs() (value []int32, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	rawValue, err := vc.readRawEncoded()
+	log.PanicIf(err)
+
+	value, err = parser.ParseSignedLongs(rawValue, vc.unitCount, vc.byteOrder)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func (vc *ValueContext) ReadSignedRationals() (value []SignedRational, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	rawValue, err := vc.readRawEncoded()
+	log.PanicIf(err)
+
+	value, err = parser.ParseSignedRationals(rawValue, vc.unitCount, vc.byteOrder)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+// Values knows how to resolve the given value. This value is always a list
+// (undefined-values aside), so we're named accordingly.
+//
+// Since this method lacks the information to process unknown-type tags (e.g.
+// byte-order, tag-ID, IFD type), it will return an error if attempted. See
+// `Undefined()`.
+func (vc *ValueContext) Values() (values interface{}, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	if vc.tagType == TypeByte {
+		values, err = vc.ReadBytes()
+		log.PanicIf(err)
+	} else if vc.tagType == TypeAscii {
+		values, err = vc.ReadAscii()
+		log.PanicIf(err)
+	} else if vc.tagType == TypeAsciiNoNul {
+		values, err = vc.ReadAsciiNoNul()
+		log.PanicIf(err)
+	} else if vc.tagType == TypeShort {
+		values, err = vc.ReadShorts()
+		log.PanicIf(err)
+	} else if vc.tagType == TypeLong {
+		values, err = vc.ReadLongs()
+		log.PanicIf(err)
+	} else if vc.tagType == TypeRational {
+		values, err = vc.ReadRationals()
+		log.PanicIf(err)
+	} else if vc.tagType == TypeSignedLong {
+		values, err = vc.ReadSignedLongs()
+		log.PanicIf(err)
+	} else if vc.tagType == TypeSignedRational {
+		values, err = vc.ReadSignedRationals()
+		log.PanicIf(err)
+	} else if vc.tagType == TypeUndefined {
+		log.Panicf("will not parse undefined-type value")
+
+		// Never called.
+		return nil, nil
+	} else {
+		log.Panicf("value of type [%s] is unparseable", vc.tagType)
+
+		// Never called.
+		return nil, nil
+	}
+
+	return values, nil
+}
+
+// Undefined attempts to identify and decode supported undefined-type fields.
+// This is the primary, preferred interface to reading undefined values.
+func (vc *ValueContext) Undefined() (value interface{}, err error) {
+	defer func() {
+		if state := recover(); state != nil {
+			err = log.Wrap(state.(error))
+		}
+	}()
+
+	value, err = UndefinedValue(vc.ifdPath, vc.tagId, vc, vc.byteOrder)
+	log.PanicIf(err)
+
+	return value, nil
+}
+
+func init() {
+	parser = &Parser{}
+}
diff --git a/v2/value_context_test.go b/v2/value_context_test.go
new file mode 100644
index 0000000..3d4df86
--- /dev/null
+++ b/v2/value_context_test.go
@@ -0,0 +1,112 @@
+package exif
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/dsoprea/go-logging"
+)
+
+func TestValueContext_ReadAscii(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Test failure.")
+		}
+	}()
+
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	ifd := index.RootIfd
+
+	var ite *IfdTagEntry
+	for _, thisIte := range ifd.Entries {
+		if thisIte.TagId == 0x0110 {
+			ite = thisIte
+			break
+		}
+	}
+
+	if ite == nil {
+		t.Fatalf("Tag not found.")
+	}
+
+	valueContext := ifd.GetValueContext(ite)
+
+	decodedString, err := valueContext.ReadAscii()
+	log.PanicIf(err)
+
+	decodedBytes := []byte(decodedString)
+
+	expected := []byte("Canon EOS 5D Mark III")
+
+	if bytes.Compare(decodedBytes, expected) != 0 {
+		t.Fatalf("Decoded bytes not correct.")
+	}
+}
+
+func TestValueContext_Undefined(t *testing.T) {
+	defer func() {
+		if state := recover(); state != nil {
+			err := log.Wrap(state.(error))
+			log.PrintErrorf(err, "Test failure.")
+		}
+	}()
+
+	rawExif, err := SearchFileAndExtractExif(testImageFilepath)
+	log.PanicIf(err)
+
+	im := NewIfdMapping()
+
+	err = LoadStandardIfds(im)
+	log.PanicIf(err)
+
+	ti := NewTagIndex()
+
+	_, index, err := Collect(im, ti, rawExif)
+	log.PanicIf(err)
+
+	ifdExif := index.Lookup[IfdPathStandardExif][0]
+
+	var ite *IfdTagEntry
+	for _, thisIte := range ifdExif.Entries {
+		if thisIte.TagId == 0x9000 {
+			ite = thisIte
+			break
+		}
+	}
+
+	if ite == nil {
+		t.Fatalf("Tag not found.")
+	}
+
+	valueContext := ifdExif.GetValueContext(ite)
+
+	value, err := valueContext.Undefined()
+	log.PanicIf(err)
+
+	gs, ok := value.(TagUnknownType_GeneralString)
+	if ok != true {
+		t.Fatalf("Undefined value not processed correctly.")
+	}
+
+	decodedBytes, err := gs.ValueBytes()
+	log.PanicIf(err)
+
+	expected := []byte("0230")
+
+	if bytes.Compare(decodedBytes, expected) != 0 {
+		t.Fatalf("Decoded bytes not correct.")
+	}
+}