commit 2bc6bd257a4f3273583a21083d2bc1b2e8736b2a Author: starainrt Date: Mon Jun 9 13:57:01 2025 +0800 init diff --git a/.MODULE_ROOT b/.MODULE_ROOT new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0b9358a --- /dev/null +++ b/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/README.md b/README.md new file mode 100644 index 0000000..6c981af --- /dev/null +++ b/README.md @@ -0,0 +1,212 @@ +[![Build Status](https://app.travis-ci.com/dsoprea/go-exif.svg?branch=master)](https://app.travis-ci.com/dsoprea/go-exif) +[![codecov](https://codecov.io/gh/dsoprea/go-exif/branch/master/graph/badge.svg)](https://codecov.io/gh/dsoprea/go-exif) +[![Go Report Card](https://goreportcard.com/badge/b612.me/exif)](https://goreportcard.com/report/b612.me/exif) +[![GoDoc](https://godoc.org/b612.me/exif?status.svg)](https://godoc.org/b612.me/exif) + +# Overview + +This package provides native Go functionality to parse an existing EXIF block, update an existing EXIF block, or add a new EXIF block. + + +# Getting + +To get the project and dependencies: + +``` +$ git clone https://github.com/dsoprea/go-exif.git +$ cd v3 +``` + + +# Requirements + +- Go >= 1.17: Due to a breakage with "go test", we only officially support 1.17 for testing/CI reasons. It may still work in earlier versions if such a need is critically required, however. + + +# Scope + +This project 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 how to write it back. File-specific formats are not the concern +of *go-exif*, though we provide +[exif.SearchAndExtractExif][search-and-extract-exif] and +[exif.SearchFileAndExtractExif][search-file-and-extract-exif] 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 the following projects to support the +efficient processing of the corresponding image formats: + +- [go-jpeg-image-structure](https://github.com/dsoprea/go-jpeg-image-structure) +- [go-png-image-structure](https://github.com/dsoprea/go-png-image-structure) +- [go-tiff-image-structure](https://github.com/dsoprea/go-tiff-image-structure) +- [go-heic-exif-extractor](https://github.com/dsoprea/go-heic-exif-extractor) + +See the [SetExif example in go-jpeg-image-structure][jpeg-set-exif] for +practical information on getting started with JPEG files. + + +# Usage + +The package provides a set of [working examples][examples] and is covered by +unit-tests. Please look to these for getting familiar with how to read and write +EXIF. + +Create an instance of the `Exif` type and call `Scan()` with a byte-slice, where +the first byte is the beginning of the raw EXIF data. You may pass a callback +that will be invoked for every tag or `nil` if you do not want one. If no +callback is given, you are effectively just validating the structure or parsing +of the image. + +Obviously, it is most efficient to properly parse the media file and then +provide the specific EXIF data to be parsed, but there is also a heuristic for +finding the EXIF data within the media blob, directly. This means that, at least +for testing or curiosity, **you do not have to parse or even understand the +format of image or audio file in order to find and decode the EXIF information +inside of it.** See the usage of the `SearchAndExtractExif` method in the +example. + +The library often refers to an IFD with an "IFD path" (e.g. IFD/Exif, +IFD/GPSInfo). A "fully-qualified" IFD-path is one that includes an index +describing which specific sibling IFD is being referred to if not the first one +(e.g. IFD1, the IFD where the thumbnail is expressed per the TIFF standard). + +There is an "IFD mapping" and a "tag index" that must be created and passed to +the library from the top. These contain all of the knowledge of the IFD +hierarchies and their tag-IDs (the IFD mapping) and the tags that they are +allowed to host (the tag index). There are convenience functions to load them +with the standard TIFF information, but you, alternatively, may choose +something totally different (to support parsing any kind of EXIF data that does +not follow or is not relevant to TIFF at all). + + +# Standards and Customization + +This project is configuration driven. By default, it has no knowledge of tags +and IDs until you load them prior to using (which is incorporated in the +examples). You are just as easily able to add additional custom IFDs and custom +tags for them. If desired, you could completely ignore the standard information +and load *totally* non-standard IFDs and tags. + +This would be useful for divergent implementations that add non-standard +information to images. It would also be useful if there is some need to just +store a flat list of tags in an image for simplified, proprietary usage. + + +# Reader Tool + +There is a runnable reading/dumping tool included: + +``` +$ go install b612.me/exif/command/exif-read-tool@latest +$ exif-read-tool --filepath "" +``` + +Example output: + +``` +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] +... +``` + +You can also print the raw, parsed data as JSON: + +``` +$ exif-read-tool --filepath "" -json +``` + +Example output: + +``` +[ + { + "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", +... +``` + + +# Testing + +The traditional method: + +``` +$ go test b612.me/exif/... +``` + + +# Release Notes + +## v3 Release + +This release primarily introduces an interchangeable data-layer, where any +`io.ReadSeeker` can be used to read EXIF data rather than necessarily loading +the EXIF blob into memory first. + +Several backwards-incompatible clean-ups were also included in this release. See +[releases][releases] for more information. + +## v2 Release + +Features a heavily reflowed interface that makes usage much simpler. The +undefined-type tag-processing (which affects most photographic images) has also +been overhauled and streamlined. It is now complete and stable. Adoption is +strongly encouraged. + + +# *Contributing* + +EXIF has an excellently-documented structure but there are a lot of devices and +manufacturers out there. There are only so many files that we can personally +find to test against, and most of these are images that have been generated only +in the past few years. JPEG, being the largest implementor of EXIF, has been +around for even longer (but not much). Therefore, there is a lot of +compatibility to test for. + +**If you are able to help by running the included reader-tool against all of the +EXIF-compatible files you have, it would be deeply appreciated. This is mostly +going to be JPEG files (but not all variations). If you are able to test a large +number of files (thousands or millions) then please post an issue mentioning how +many files you have processed. If you had failures, then please share them and +try to support efforts to understand them.** + +If you are able to test 100K+ files, I will give you credit on the project. The +further back in time your images reach, the higher in the list your name/company +will go. + + +# Contributors/Testing + +Thank you to the following users for solving non-trivial issues, supporting the +project with solving edge-case problems in specific images, or otherwise +providing their non-trivial time or image corpus to test go-exif: + +- [philip-firstorder](https://github.com/philip-firstorder) (200K images) +- [matchstick](https://github.com/matchstick) (102K images) + +In addition to these, it has been tested on my own collection, north of 560K +images. + +[search-and-extract-exif]: https://godoc.org/b612.me/exif#SearchAndExtractExif +[search-file-and-extract-exif]: https://godoc.org/b612.me/exif#SearchFileAndExtractExif +[jpeg-set-exif]: https://godoc.org/github.com/dsoprea/go-jpeg-image-structure#example-SegmentList-SetExif +[examples]: https://godoc.org/b612.me/exif#pkg-examples +[releases]: https://github.com/dsoprea/go-exif/releases diff --git a/assets/DC-008-2012_E.pdf b/assets/DC-008-2012_E.pdf new file mode 100644 index 0000000..e43d85c Binary files /dev/null and b/assets/DC-008-2012_E.pdf differ diff --git a/assets/DC-008-Translation-2016-E.pdf b/assets/DC-008-Translation-2016-E.pdf new file mode 100644 index 0000000..c05df44 Binary files /dev/null and b/assets/DC-008-Translation-2016-E.pdf differ diff --git a/assets/GeoTIFF_Profile_for_Georeferenced_Imagery.doc b/assets/GeoTIFF_Profile_for_Georeferenced_Imagery.doc new file mode 100644 index 0000000..9e9b7a5 Binary files /dev/null and b/assets/GeoTIFF_Profile_for_Georeferenced_Imagery.doc differ diff --git a/assets/NDM_8901.jpg b/assets/NDM_8901.jpg new file mode 100644 index 0000000..b892262 Binary files /dev/null and b/assets/NDM_8901.jpg differ diff --git a/assets/NDM_8901.jpg.exif b/assets/NDM_8901.jpg.exif new file mode 100644 index 0000000..58ec96f Binary files /dev/null and b/assets/NDM_8901.jpg.exif differ diff --git a/assets/NDM_8901.jpg.thumbnail b/assets/NDM_8901.jpg.thumbnail new file mode 100644 index 0000000..e82002b Binary files /dev/null and b/assets/NDM_8901.jpg.thumbnail differ diff --git a/assets/PNG-GROUP-DRAFT PNG proposed eXIf chunk, draft 2017-06-15.pdf b/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/assets/PNG-GROUP-DRAFT PNG proposed eXIf chunk, draft 2017-06-15.pdf differ diff --git a/assets/geotiff_example.tif b/assets/geotiff_example.tif new file mode 100644 index 0000000..a0a38aa Binary files /dev/null and b/assets/geotiff_example.tif differ diff --git a/assets/gps-2000-scaled.jpg b/assets/gps-2000-scaled.jpg new file mode 100644 index 0000000..b8fe317 Binary files /dev/null and b/assets/gps-2000-scaled.jpg differ diff --git a/assets/gps.jpg b/assets/gps.jpg new file mode 100644 index 0000000..fbb680a Binary files /dev/null and b/assets/gps.jpg differ diff --git a/assets/main_test_exif.json b/assets/main_test_exif.json new file mode 100644 index 0000000..9603c7c --- /dev/null +++ b/assets/main_test_exif.json @@ -0,0 +1,909 @@ +[ + { + "ifd_path": "IFD", + "id": 271, + "name": "Make", + "unit_count": 6, + "type_id": 2, + "type_name": "ASCII", + "value": "Canon", + "value_bytes": "Q2Fub24A", + "formatted_first": "Canon", + "formatted": "Canon", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD", + "id": 272, + "name": "Model", + "unit_count": 22, + "type_id": 2, + "type_name": "ASCII", + "value": "Canon EOS 5D Mark III", + "value_bytes": "Q2Fub24gRU9TIDVEIE1hcmsgSUlJAA==", + "formatted_first": "Canon EOS 5D Mark III", + "formatted": "Canon EOS 5D Mark III", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD", + "id": 274, + "name": "Orientation", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 1 + ], + "value_bytes": "AQA=", + "formatted_first": "1", + "formatted": "[1]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD", + "id": 282, + "name": "XResolution", + "unit_count": 1, + "type_id": 5, + "type_name": "RATIONAL", + "value": [ + { + "Numerator": 72, + "Denominator": 1 + } + ], + "value_bytes": "SAAAAAEAAAA=", + "formatted_first": "72/1", + "formatted": "[72/1]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD", + "id": 283, + "name": "YResolution", + "unit_count": 1, + "type_id": 5, + "type_name": "RATIONAL", + "value": [ + { + "Numerator": 72, + "Denominator": 1 + } + ], + "value_bytes": "SAAAAAEAAAA=", + "formatted_first": "72/1", + "formatted": "[72/1]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD", + "id": 296, + "name": "ResolutionUnit", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 2 + ], + "value_bytes": "AgA=", + "formatted_first": "2", + "formatted": "[2]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD", + "id": 306, + "name": "DateTime", + "unit_count": 20, + "type_id": 2, + "type_name": "ASCII", + "value": "2017:12:02 08:18:50", + "value_bytes": "MjAxNzoxMjowMiAwODoxODo1MAA=", + "formatted_first": "2017:12:02 08:18:50", + "formatted": "2017:12:02 08:18:50", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD", + "id": 315, + "name": "Artist", + "unit_count": 1, + "type_id": 2, + "type_name": "ASCII", + "value": "", + "value_bytes": "AA==", + "formatted_first": "", + "formatted": "", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD", + "id": 531, + "name": "YCbCrPositioning", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 2 + ], + "value_bytes": "AgA=", + "formatted_first": "2", + "formatted": "[2]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD", + "id": 33432, + "name": "Copyright", + "unit_count": 1, + "type_id": 2, + "type_name": "ASCII", + "value": "", + "value_bytes": "AA==", + "formatted_first": "", + "formatted": "", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD", + "id": 34665, + "name": "ExifTag", + "unit_count": 1, + "type_id": 4, + "type_name": "LONG", + "value": [ + 360 + ], + "value_bytes": "aAEAAA==", + "formatted_first": "360", + "formatted": "[360]", + "child_ifd_path": "IFD/Exif" + }, + { + "ifd_path": "IFD/Exif", + "id": 33434, + "name": "ExposureTime", + "unit_count": 1, + "type_id": 5, + "type_name": "RATIONAL", + "value": [ + { + "Numerator": 1, + "Denominator": 640 + } + ], + "value_bytes": "AQAAAIACAAA=", + "formatted_first": "1/640", + "formatted": "[1/640]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 33437, + "name": "FNumber", + "unit_count": 1, + "type_id": 5, + "type_name": "RATIONAL", + "value": [ + { + "Numerator": 4, + "Denominator": 1 + } + ], + "value_bytes": "BAAAAAEAAAA=", + "formatted_first": "4/1", + "formatted": "[4/1]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 34850, + "name": "ExposureProgram", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 4 + ], + "value_bytes": "BAA=", + "formatted_first": "4", + "formatted": "[4]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 34855, + "name": "ISOSpeedRatings", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 1600 + ], + "value_bytes": "QAY=", + "formatted_first": "1600", + "formatted": "[1600]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 34864, + "name": "SensitivityType", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 2 + ], + "value_bytes": "AgA=", + "formatted_first": "2", + "formatted": "[2]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 34866, + "name": "RecommendedExposureIndex", + "unit_count": 1, + "type_id": 4, + "type_name": "LONG", + "value": [ + 1600 + ], + "value_bytes": "QAYAAA==", + "formatted_first": "1600", + "formatted": "[1600]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 36864, + "name": "ExifVersion", + "unit_count": 4, + "type_id": 7, + "type_name": "UNDEFINED", + "value": { + "ExifVersion": "0230" + }, + "value_bytes": "MDIzMA==", + "formatted_first": "0230", + "formatted": "0230", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 36867, + "name": "DateTimeOriginal", + "unit_count": 20, + "type_id": 2, + "type_name": "ASCII", + "value": "2017:12:02 08:18:50", + "value_bytes": "MjAxNzoxMjowMiAwODoxODo1MAA=", + "formatted_first": "2017:12:02 08:18:50", + "formatted": "2017:12:02 08:18:50", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 36868, + "name": "DateTimeDigitized", + "unit_count": 20, + "type_id": 2, + "type_name": "ASCII", + "value": "2017:12:02 08:18:50", + "value_bytes": "MjAxNzoxMjowMiAwODoxODo1MAA=", + "formatted_first": "2017:12:02 08:18:50", + "formatted": "2017:12:02 08:18:50", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 37121, + "name": "ComponentsConfiguration", + "unit_count": 4, + "type_id": 7, + "type_name": "UNDEFINED", + "value": { + "ConfigurationId": 2, + "ConfigurationBytes": "AQIDAA==" + }, + "value_bytes": "AQIDAA==", + "formatted_first": "Exif9101ComponentsConfiguration\u003cID=[YCBCR] BYTES=[1 2 3 0]\u003e", + "formatted": "Exif9101ComponentsConfiguration\u003cID=[YCBCR] BYTES=[1 2 3 0]\u003e", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 37377, + "name": "ShutterSpeedValue", + "unit_count": 1, + "type_id": 10, + "type_name": "SRATIONAL", + "value": [ + { + "Numerator": 614400, + "Denominator": 65536 + } + ], + "value_bytes": "AGAJAAAAAQA=", + "formatted_first": "614400/65536", + "formatted": "[614400/65536]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 37378, + "name": "ApertureValue", + "unit_count": 1, + "type_id": 5, + "type_name": "RATIONAL", + "value": [ + { + "Numerator": 262144, + "Denominator": 65536 + } + ], + "value_bytes": "AAAEAAAAAQA=", + "formatted_first": "262144/65536", + "formatted": "[262144/65536]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 37380, + "name": "ExposureBiasValue", + "unit_count": 1, + "type_id": 10, + "type_name": "SRATIONAL", + "value": [ + { + "Numerator": 0, + "Denominator": 1 + } + ], + "value_bytes": "AAAAAAEAAAA=", + "formatted_first": "0/1", + "formatted": "[0/1]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 37383, + "name": "MeteringMode", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 5 + ], + "value_bytes": "BQA=", + "formatted_first": "5", + "formatted": "[5]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 37385, + "name": "Flash", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 16 + ], + "value_bytes": "EAA=", + "formatted_first": "16", + "formatted": "[16]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 37386, + "name": "FocalLength", + "unit_count": 1, + "type_id": 5, + "type_name": "RATIONAL", + "value": [ + { + "Numerator": 16, + "Denominator": 1 + } + ], + "value_bytes": "EAAAAAEAAAA=", + "formatted_first": "16/1", + "formatted": "[16/1]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 37500, + "name": "MakerNote", + "unit_count": 8152, + "type_id": 7, + "type_name": "UNDEFINED", + "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_bytes": "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==", + "formatted_first": "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", + "formatted": "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", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 37510, + "name": "UserComment", + "unit_count": 264, + "type_id": 7, + "type_name": "UNDEFINED", + "value": { + "EncodingType": 3, + "EncodingBytes": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "value_bytes": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "formatted_first": "UserComment\u003cSIZE=(256) ENCODING=[UNDEFINED] V=[0 0 0 0 0 0 0 0]... LEN=(256)\u003e", + "formatted": "UserComment\u003cSIZE=(256) ENCODING=[UNDEFINED] V=[0 0 0 0 0 0 0 0]... LEN=(256)\u003e", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 37520, + "name": "SubSecTime", + "unit_count": 3, + "type_id": 2, + "type_name": "ASCII", + "value": "00", + "value_bytes": "MDAA", + "formatted_first": "00", + "formatted": "00", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 37521, + "name": "SubSecTimeOriginal", + "unit_count": 3, + "type_id": 2, + "type_name": "ASCII", + "value": "00", + "value_bytes": "MDAA", + "formatted_first": "00", + "formatted": "00", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 37522, + "name": "SubSecTimeDigitized", + "unit_count": 3, + "type_id": 2, + "type_name": "ASCII", + "value": "00", + "value_bytes": "MDAA", + "formatted_first": "00", + "formatted": "00", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 40960, + "name": "FlashpixVersion", + "unit_count": 4, + "type_id": 7, + "type_name": "UNDEFINED", + "value": { + "FlashpixVersion": "0100" + }, + "value_bytes": "MDEwMA==", + "formatted_first": "0100", + "formatted": "0100", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 40961, + "name": "ColorSpace", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 1 + ], + "value_bytes": "AQA=", + "formatted_first": "1", + "formatted": "[1]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 40962, + "name": "PixelXDimension", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 3840 + ], + "value_bytes": "AA8=", + "formatted_first": "3840", + "formatted": "[3840]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 40963, + "name": "PixelYDimension", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 2560 + ], + "value_bytes": "AAo=", + "formatted_first": "2560", + "formatted": "[2560]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 40965, + "name": "InteroperabilityTag", + "unit_count": 1, + "type_id": 4, + "type_name": "LONG", + "value": [ + 9326 + ], + "value_bytes": "biQAAA==", + "formatted_first": "9326", + "formatted": "[9326]", + "child_ifd_path": "IFD/Exif/Iop" + }, + { + "ifd_path": "IFD/Exif/Iop", + "id": 1, + "name": "InteroperabilityIndex", + "unit_count": 4, + "type_id": 2, + "type_name": "ASCII", + "value": "R98", + "value_bytes": "Ujk4AA==", + "formatted_first": "R98", + "formatted": "R98", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif/Iop", + "id": 2, + "name": "InteroperabilityVersion", + "unit_count": 4, + "type_id": 7, + "type_name": "UNDEFINED", + "value": { + "InteropVersion": "0100" + }, + "value_bytes": "MDEwMA==", + "formatted_first": "0100", + "formatted": "0100", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 41486, + "name": "FocalPlaneXResolution", + "unit_count": 1, + "type_id": 5, + "type_name": "RATIONAL", + "value": [ + { + "Numerator": 3840000, + "Denominator": 1461 + } + ], + "value_bytes": "AJg6ALUFAAA=", + "formatted_first": "3840000/1461", + "formatted": "[3840000/1461]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 41487, + "name": "FocalPlaneYResolution", + "unit_count": 1, + "type_id": 5, + "type_name": "RATIONAL", + "value": [ + { + "Numerator": 2560000, + "Denominator": 972 + } + ], + "value_bytes": "ABAnAMwDAAA=", + "formatted_first": "2560000/972", + "formatted": "[2560000/972]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 41488, + "name": "FocalPlaneResolutionUnit", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 2 + ], + "value_bytes": "AgA=", + "formatted_first": "2", + "formatted": "[2]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 41985, + "name": "CustomRendered", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 0 + ], + "value_bytes": "AAA=", + "formatted_first": "0", + "formatted": "[0]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 41986, + "name": "ExposureMode", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 0 + ], + "value_bytes": "AAA=", + "formatted_first": "0", + "formatted": "[0]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 41987, + "name": "WhiteBalance", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 0 + ], + "value_bytes": "AAA=", + "formatted_first": "0", + "formatted": "[0]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 41990, + "name": "SceneCaptureType", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 0 + ], + "value_bytes": "AAA=", + "formatted_first": "0", + "formatted": "[0]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 42032, + "name": "CameraOwnerName", + "unit_count": 1, + "type_id": 2, + "type_name": "ASCII", + "value": "", + "value_bytes": "AA==", + "formatted_first": "", + "formatted": "", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 42033, + "name": "BodySerialNumber", + "unit_count": 13, + "type_id": 2, + "type_name": "ASCII", + "value": "063024020097", + "value_bytes": "MDYzMDI0MDIwMDk3AA==", + "formatted_first": "063024020097", + "formatted": "063024020097", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 42034, + "name": "LensSpecification", + "unit_count": 4, + "type_id": 5, + "type_name": "RATIONAL", + "value": [ + { + "Numerator": 16, + "Denominator": 1 + }, + { + "Numerator": 35, + "Denominator": 1 + }, + { + "Numerator": 0, + "Denominator": 1 + }, + { + "Numerator": 0, + "Denominator": 1 + } + ], + "value_bytes": "EAAAAAEAAAAjAAAAAQAAAAAAAAABAAAAAAAAAAEAAAA=", + "formatted_first": "16/1...", + "formatted": "[16/1 35/1 0/1 0/1]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 42036, + "name": "LensModel", + "unit_count": 22, + "type_id": 2, + "type_name": "ASCII", + "value": "EF16-35mm f/4L IS USM", + "value_bytes": "RUYxNi0zNW1tIGYvNEwgSVMgVVNNAA==", + "formatted_first": "EF16-35mm f/4L IS USM", + "formatted": "EF16-35mm f/4L IS USM", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD/Exif", + "id": 42037, + "name": "LensSerialNumber", + "unit_count": 11, + "type_id": 2, + "type_name": "ASCII", + "value": "2400001068", + "value_bytes": "MjQwMDAwMTA2OAA=", + "formatted_first": "2400001068", + "formatted": "2400001068", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD", + "id": 34853, + "name": "GPSTag", + "unit_count": 1, + "type_id": 4, + "type_name": "LONG", + "value": [ + 9554 + ], + "value_bytes": "UiUAAA==", + "formatted_first": "9554", + "formatted": "[9554]", + "child_ifd_path": "IFD/GPSInfo" + }, + { + "ifd_path": "IFD/GPSInfo", + "id": 0, + "name": "GPSVersionID", + "unit_count": 4, + "type_id": 1, + "type_name": "BYTE", + "value": "AgMAAA==", + "value_bytes": "AgMAAA==", + "formatted_first": "02 03 00 00", + "formatted": "02 03 00 00", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD1", + "id": 259, + "name": "Compression", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 6 + ], + "value_bytes": "BgA=", + "formatted_first": "6", + "formatted": "[6]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD1", + "id": 282, + "name": "XResolution", + "unit_count": 1, + "type_id": 5, + "type_name": "RATIONAL", + "value": [ + { + "Numerator": 72, + "Denominator": 1 + } + ], + "value_bytes": "SAAAAAEAAAA=", + "formatted_first": "72/1", + "formatted": "[72/1]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD1", + "id": 283, + "name": "YResolution", + "unit_count": 1, + "type_id": 5, + "type_name": "RATIONAL", + "value": [ + { + "Numerator": 72, + "Denominator": 1 + } + ], + "value_bytes": "SAAAAAEAAAA=", + "formatted_first": "72/1", + "formatted": "[72/1]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD1", + "id": 296, + "name": "ResolutionUnit", + "unit_count": 1, + "type_id": 3, + "type_name": "SHORT", + "value": [ + 2 + ], + "value_bytes": "AgA=", + "formatted_first": "2", + "formatted": "[2]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD1", + "id": 513, + "name": "JPEGInterchangeFormat", + "unit_count": 1, + "type_id": 4, + "type_name": "LONG", + "value": [ + 11444 + ], + "value_bytes": "tCwAAA==", + "formatted_first": "11444", + "formatted": "[11444]", + "child_ifd_path": "" + }, + { + "ifd_path": "IFD1", + "id": 514, + "name": "JPEGInterchangeFormatLength", + "unit_count": 1, + "type_id": 4, + "type_name": "LONG", + "value": [ + 21491 + ], + "value_bytes": "81MAAA==", + "formatted_first": "21491", + "formatted": "[21491]", + "child_ifd_path": "" + } +] diff --git a/assets/raw_tags/requirements.txt b/assets/raw_tags/requirements.txt new file mode 100644 index 0000000..58d3608 --- /dev/null +++ b/assets/raw_tags/requirements.txt @@ -0,0 +1 @@ +ruamel.yaml diff --git a/assets/raw_tags/tags.html b/assets/raw_tags/tags.html new file mode 100644 index 0000000..85cd836 --- /dev/null +++ b/assets/raw_tags/tags.html @@ -0,0 +1,2492 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tag (hex)Tag (dec)IFDKeyTypeTag description
0x000b11ImageExif.Image.ProcessingSoftwareAsciiThe name and version of the software used to post-process the picture.
0x00fe254ImageExif.Image.NewSubfileTypeLongA general indication of the kind of data contained in this subfile.
0x00ff255ImageExif.Image.SubfileTypeShortA general indication of the kind of data contained in this subfile. This field is deprecated. The NewSubfileType field should be used instead.
0x0100256ImageExif.Image.ImageWidthLongThe 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.
0x0101257ImageExif.Image.ImageLengthLongThe number of rows of image data. In JPEG compressed data a JPEG marker is used instead of this tag.
0x0102258ImageExif.Image.BitsPerSampleShortThe 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 <SamplesPerPixel>. In JPEG compressed data a JPEG marker is used instead of this tag.
0x0103259ImageExif.Image.CompressionShortThe 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.
0x0106262ImageExif.Image.PhotometricInterpretationShortThe pixel composition. In JPEG compressed data a JPEG marker is used instead of this tag.
0x0107263ImageExif.Image.ThresholdingShortFor black and white TIFF files that represent shades of gray, the technique used to convert from gray to black and white pixels.
0x0108264ImageExif.Image.CellWidthShortThe width of the dithering or halftoning matrix used to create a dithered or halftoned bilevel file.
0x0109265ImageExif.Image.CellLengthShortThe length of the dithering or halftoning matrix used to create a dithered or halftoned bilevel file.
0x010a266ImageExif.Image.FillOrderShortThe logical order of bits within a byte
0x010d269ImageExif.Image.DocumentNameAsciiThe name of the document from which this image was scanned
0x010e270ImageExif.Image.ImageDescriptionAsciiA 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 <UserComment> is to be used.
0x010f271ImageExif.Image.MakeAsciiThe 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.
0x0110272ImageExif.Image.ModelAsciiThe 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.
0x0111273ImageExif.Image.StripOffsetsLongFor 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 <RowsPerStrip> and <StripByteCounts>.
0x0112274ImageExif.Image.OrientationShortThe image orientation viewed in terms of rows and columns.
0x0115277ImageExif.Image.SamplesPerPixelShortThe 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.
0x0116278ImageExif.Image.RowsPerStripLongThe 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 <StripOffsets> and <StripByteCounts>.
0x0117279ImageExif.Image.StripByteCountsLongThe total number of bytes in each strip. With JPEG compressed data this designation is not needed and is omitted.
0x011a282ImageExif.Image.XResolutionRationalThe number of pixels per <ResolutionUnit> in the <ImageWidth> direction. When the image resolution is unknown, 72 [dpi] is designated.
0x011b283ImageExif.Image.YResolutionRationalThe number of pixels per <ResolutionUnit> in the <ImageLength> direction. The same value as <XResolution> is designated.
0x011c284ImageExif.Image.PlanarConfigurationShortIndicates 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.
0x0122290ImageExif.Image.GrayResponseUnitShortThe precision of the information contained in the GrayResponseCurve.
0x0123291ImageExif.Image.GrayResponseCurveShortFor grayscale data, the optical density of each possible pixel value.
0x0124292ImageExif.Image.T4OptionsLongT.4-encoding options.
0x0125293ImageExif.Image.T6OptionsLongT.6-encoding options.
0x0128296ImageExif.Image.ResolutionUnitShortThe unit for measuring <XResolution> and <YResolution>. The same unit is used for both <XResolution> and <YResolution>. If the image resolution is unknown, 2 (inches) is designated.
0x0129297ImageExif.Image.PageNumberShortThe page number of the page from which this image was scanned.
0x012d301ImageExif.Image.TransferFunctionShortA 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 (<ColorSpace>).
0x0131305ImageExif.Image.SoftwareAsciiThis 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.
0x0132306ImageExif.Image.DateTimeAsciiThe date and time of image creation. In Exif standard, it is the date and time the file was changed.
0x013b315ImageExif.Image.ArtistAsciiThis 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"
0x013c316ImageExif.Image.HostComputerAsciiThis tag records information about the host computer used to generate the image.
0x013d317ImageExif.Image.PredictorShortA predictor is a mathematical operator that is applied to the image data before an encoding scheme is applied.
0x013e318ImageExif.Image.WhitePointRationalThe chromaticity of the white point of the image. Normally this tag is not necessary, since color space is specified in the colorspace information tag (<ColorSpace>).
0x013f319ImageExif.Image.PrimaryChromaticitiesRationalThe chromaticity of the three primary colors of the image. Normally this tag is not necessary, since colorspace is specified in the colorspace information tag (<ColorSpace>).
0x0140320ImageExif.Image.ColorMapShortA 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.
0x0141321ImageExif.Image.HalftoneHintsShortThe 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.
0x0142322ImageExif.Image.TileWidthShortThe tile width in pixels. This is the number of columns in each tile.
0x0143323ImageExif.Image.TileLengthShortThe tile length (height) in pixels. This is the number of rows in each tile.
0x0144324ImageExif.Image.TileOffsetsShortFor 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.
0x0145325ImageExif.Image.TileByteCountsShortFor each tile, the number of (compressed) bytes in that tile. See TileOffsets for a description of how the byte counts are ordered.
0x014a330ImageExif.Image.SubIFDsLongDefined by Adobe Corporation to enable TIFF Trees within a TIFF file.
0x014c332ImageExif.Image.InkSetShortThe set of inks used in a separated (PhotometricInterpretation=5) image.
0x014d333ImageExif.Image.InkNamesAsciiThe name of each ink used in a separated (PhotometricInterpretation=5) image.
0x014e334ImageExif.Image.NumberOfInksShortThe number of inks. Usually equal to SamplesPerPixel, unless there are extra samples.
0x0150336ImageExif.Image.DotRangeByteThe component values that correspond to a 0% dot and 100% dot.
0x0151337ImageExif.Image.TargetPrinterAsciiA description of the printing environment for which this separation is intended.
0x0152338ImageExif.Image.ExtraSamplesShortSpecifies that each pixel has m extra components whose interpretation is defined by one of the values listed below.
0x0153339ImageExif.Image.SampleFormatShortThis field specifies how to interpret each data sample in a pixel.
0x0154340ImageExif.Image.SMinSampleValueShortThis field specifies the minimum sample value.
0x0155341ImageExif.Image.SMaxSampleValueShortThis field specifies the maximum sample value.
0x0156342ImageExif.Image.TransferRangeShortExpands the range of the TransferFunction
0x0157343ImageExif.Image.ClipPathByteA TIFF ClipPath is intended to mirror the essentials of PostScript's path creation functionality.
0x0158344ImageExif.Image.XClipPathUnitsSShortThe number of units that span the width of the image, in terms of integer ClipPath coordinates.
0x0159345ImageExif.Image.YClipPathUnitsSShortThe number of units that span the height of the image, in terms of integer ClipPath coordinates.
0x015a346ImageExif.Image.IndexedShortIndexed 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.
0x015b347ImageExif.Image.JPEGTablesUndefinedThis optional tag may be used to encode the JPEG quantization and Huffman tables for subsequent use by the JPEG decompression process.
0x015f351ImageExif.Image.OPIProxyShortOPIProxy gives information concerning whether this image is a low-resolution proxy of a high-resolution image (Adobe OPI).
0x0200512ImageExif.Image.JPEGProcLongThis field indicates the process used to produce the compressed data
0x0201513ImageExif.Image.JPEGInterchangeFormatLongThe offset to the start byte (SOI) of JPEG compressed thumbnail data. This is not used for primary image JPEG data.
0x0202514ImageExif.Image.JPEGInterchangeFormatLengthLongThe 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.
0x0203515ImageExif.Image.JPEGRestartIntervalShortThis Field indicates the length of the restart interval used in the compressed image data.
0x0205517ImageExif.Image.JPEGLosslessPredictorsShortThis Field points to a list of lossless predictor-selection values, one per component.
0x0206518ImageExif.Image.JPEGPointTransformsShortThis Field points to a list of point transform values, one per component.
0x0207519ImageExif.Image.JPEGQTablesLongThis Field points to a list of offsets to the quantization tables, one per component.
0x0208520ImageExif.Image.JPEGDCTablesLongThis Field points to a list of offsets to the DC Huffman tables or the lossless Huffman tables, one per component.
0x0209521ImageExif.Image.JPEGACTablesLongThis Field points to a list of offsets to the Huffman AC tables, one per component.
0x0211529ImageExif.Image.YCbCrCoefficientsRationalThe 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.
0x0212530ImageExif.Image.YCbCrSubSamplingShortThe sampling ratio of chrominance components in relation to the luminance component. In JPEG compressed data a JPEG marker is used instead of this tag.
0x0213531ImageExif.Image.YCbCrPositioningShortThe 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 <YCbCrPositioning>, 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.
0x0214532ImageExif.Image.ReferenceBlackWhiteRationalThe 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.
0x02bc700ImageExif.Image.XMLPacketByteXMP Metadata (Adobe technote 9-14-02)
0x474618246ImageExif.Image.RatingShortRating tag used by Windows
0x474918249ImageExif.Image.RatingPercentShortRating tag used by Windows, value in percent
0x800d32781ImageExif.Image.ImageIDAsciiImageID is the full pathname of the original, high-resolution image, or any other identifying string that uniquely identifies the original image (Adobe OPI).
0x828d33421ImageExif.Image.CFARepeatPatternDimShortContains two values representing the minimum rows and columns to define the repeating patterns of the color filter array
0x828e33422ImageExif.Image.CFAPatternByteIndicates 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
0x828f33423ImageExif.Image.BatteryLevelRationalContains a value of the battery level as a fraction or string
0x829833432ImageExif.Image.CopyrightAsciiCopyright 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.
0x829a33434ImageExif.Image.ExposureTimeRationalExposure time, given in seconds.
0x829d33437ImageExif.Image.FNumberRationalThe F number.
0x83bb33723ImageExif.Image.IPTCNAALongContains an IPTC/NAA record
0x864934377ImageExif.Image.ImageResourcesByteContains information embedded by the Adobe Photoshop application
0x876934665ImageExif.Image.ExifTagLongA 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.
0x877334675ImageExif.Image.InterColorProfileUndefinedContains an InterColor Consortium (ICC) format color space characterization/profile
0x882234850ImageExif.Image.ExposureProgramShortThe class of the program used by the camera to set exposure when the picture is taken.
0x882434852ImageExif.Image.SpectralSensitivityAsciiIndicates the spectral sensitivity of each channel of the camera used.
0x882534853ImageExif.Image.GPSTagLongA pointer to the GPS Info IFD. The Interoperability structure of the GPS Info IFD, like that of Exif IFD, has no image data.
0x882734855ImageExif.Image.ISOSpeedRatingsShortIndicates the ISO Speed and ISO Latitude of the camera or input device as specified in ISO 12232.
0x882834856ImageExif.Image.OECFUndefinedIndicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524.
0x882934857ImageExif.Image.InterlaceShortIndicates the field number of multifield images.
0x882a34858ImageExif.Image.TimeZoneOffsetSShortThis 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.
0x882b34859ImageExif.Image.SelfTimerModeShortNumber of seconds image capture was delayed from button press.
0x900336867ImageExif.Image.DateTimeOriginalAsciiThe date and time when the original image data was generated.
0x910237122ImageExif.Image.CompressedBitsPerPixelRationalSpecific to compressed data; states the compressed bits per pixel.
0x920137377ImageExif.Image.ShutterSpeedValueSRationalShutter speed.
0x920237378ImageExif.Image.ApertureValueRationalThe lens aperture.
0x920337379ImageExif.Image.BrightnessValueSRationalThe value of brightness.
0x920437380ImageExif.Image.ExposureBiasValueSRationalThe exposure bias.
0x920537381ImageExif.Image.MaxApertureValueRationalThe smallest F number of the lens.
0x920637382ImageExif.Image.SubjectDistanceSRationalThe distance to the subject, given in meters.
0x920737383ImageExif.Image.MeteringModeShortThe metering mode.
0x920837384ImageExif.Image.LightSourceShortThe kind of light source.
0x920937385ImageExif.Image.FlashShortIndicates the status of flash when the image was shot.
0x920a37386ImageExif.Image.FocalLengthRationalThe actual focal length of the lens, in mm.
0x920b37387ImageExif.Image.FlashEnergyRationalAmount of flash energy (BCPS).
0x920c37388ImageExif.Image.SpatialFrequencyResponseUndefinedSFR of the camera.
0x920d37389ImageExif.Image.NoiseUndefinedNoise measurement values.
0x920e37390ImageExif.Image.FocalPlaneXResolutionRationalNumber of pixels per FocalPlaneResolutionUnit (37392) in ImageWidth direction for main image.
0x920f37391ImageExif.Image.FocalPlaneYResolutionRationalNumber of pixels per FocalPlaneResolutionUnit (37392) in ImageLength direction for main image.
0x921037392ImageExif.Image.FocalPlaneResolutionUnitShortUnit of measurement for FocalPlaneXResolution(37390) and FocalPlaneYResolution(37391).
0x921137393ImageExif.Image.ImageNumberLongNumber assigned to an image, e.g., in a chained image burst.
0x921237394ImageExif.Image.SecurityClassificationAsciiSecurity classification assigned to the image.
0x921337395ImageExif.Image.ImageHistoryAsciiRecord of what has been done to the image.
0x921437396ImageExif.Image.SubjectLocationShortIndicates the location and area of the main subject in the overall scene.
0x921537397ImageExif.Image.ExposureIndexRationalEncodes the camera exposure index setting when image was captured.
0x921637398ImageExif.Image.TIFFEPStandardIDByteContains four ASCII characters representing the TIFF/EP standard version of a TIFF/EP file, eg '1', '0', '0', '0'
0x921737399ImageExif.Image.SensingMethodShortType of image sensor.
0x9c9b40091ImageExif.Image.XPTitleByteTitle tag used by Windows, encoded in UCS2
0x9c9c40092ImageExif.Image.XPCommentByteComment tag used by Windows, encoded in UCS2
0x9c9d40093ImageExif.Image.XPAuthorByteAuthor tag used by Windows, encoded in UCS2
0x9c9e40094ImageExif.Image.XPKeywordsByteKeywords tag used by Windows, encoded in UCS2
0x9c9f40095ImageExif.Image.XPSubjectByteSubject tag used by Windows, encoded in UCS2
0xc4a550341ImageExif.Image.PrintImageMatchingUndefinedPrint Image Matching, description needed.
0xc61250706ImageExif.Image.DNGVersionByteThis 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.
0xc61350707ImageExif.Image.DNGBackwardVersionByteThis 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.
0xc61450708ImageExif.Image.UniqueCameraModelAsciiDefines 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.
0xc61550709ImageExif.Image.LocalizedCameraModelByteSimilar to the UniqueCameraModel field, except the name can be localized for different markets to match the localization of the camera name.
0xc61650710ImageExif.Image.CFAPlaneColorByteProvides 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.
0xc61750711ImageExif.Image.CFALayoutShortDescribes the spatial layout of the CFA.
0xc61850712ImageExif.Image.LinearizationTableShortDescribes 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.
0xc61950713ImageExif.Image.BlackLevelRepeatDimShortSpecifies repeat pattern size for the BlackLevel tag.
0xc61a50714ImageExif.Image.BlackLevelRationalSpecifies 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.
0xc61b50715ImageExif.Image.BlackLevelDeltaHSRationalIf 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.
0xc61c50716ImageExif.Image.BlackLevelDeltaVSRationalIf 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.
0xc61d50717ImageExif.Image.WhiteLevelShortThis 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.
0xc61e50718ImageExif.Image.DefaultScaleRationalDefaultScale 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.
0xc61f50719ImageExif.Image.DefaultCropOriginShortRaw 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.
0xc62050720ImageExif.Image.DefaultCropSizeShortRaw 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).
0xc62150721ImageExif.Image.ColorMatrix1SRationalColorMatrix1 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.
0xc62250722ImageExif.Image.ColorMatrix2SRationalColorMatrix2 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.
0xc62350723ImageExif.Image.CameraCalibration1SRationalCameraCalibration1 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.
0xc62450724ImageExif.Image.CameraCalibration2SRationalCameraCalibration2 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.
0xc62550725ImageExif.Image.ReductionMatrix1SRationalReductionMatrix1 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.
0xc62650726ImageExif.Image.ReductionMatrix2SRationalReductionMatrix2 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.
0xc62750727ImageExif.Image.AnalogBalanceRationalNormally 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.
0xc62850728ImageExif.Image.AsShotNeutralShortSpecifies 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.
0xc62950729ImageExif.Image.AsShotWhiteXYRationalSpecifies 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.
0xc62a50730ImageExif.Image.BaselineExposureSRationalCamera 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.
0xc62b50731ImageExif.Image.BaselineNoiseRationalSpecifies 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.
0xc62c50732ImageExif.Image.BaselineSharpnessRationalSpecifies 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.
0xc62d50733ImageExif.Image.BayerGreenSplitLongOnly 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).
0xc62e50734ImageExif.Image.LinearResponseLimitRationalSome 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.
0xc62f50735ImageExif.Image.CameraSerialNumberAsciiCameraSerialNumber contains the serial number of the camera or camera body that captured the image.
0xc63050736ImageExif.Image.LensInfoRationalContains information about the lens that captured the image. If the minimum f-stops are unknown, they should be encoded as 0/0.
0xc63150737ImageExif.Image.ChromaBlurRadiusRationalChromaBlurRadius 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.
0xc63250738ImageExif.Image.AntiAliasStrengthRationalProvides 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).
0xc63350739ImageExif.Image.ShadowScaleSRationalThis tag is used by Adobe Camera Raw to control the sensitivity of its 'Shadows' slider.
0xc63450740ImageExif.Image.DNGPrivateDataByteProvides 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.
0xc63550741ImageExif.Image.MakerNoteSafetyShortMakerNoteSafety 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.
0xc65a50778ImageExif.Image.CalibrationIlluminant1ShortThe 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.
0xc65b50779ImageExif.Image.CalibrationIlluminant2ShortThe 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).
0xc65c50780ImageExif.Image.BestQualityScaleRationalFor 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.
0xc65d50781ImageExif.Image.RawDataUniqueIDByteThis 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.
0xc68b50827ImageExif.Image.OriginalRawFileNameByteIf the DNG file was converted from a non-DNG raw file, then this tag contains the file name of that original raw file.
0xc68c50828ImageExif.Image.OriginalRawFileDataUndefinedIf 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.
0xc68d50829ImageExif.Image.ActiveAreaShortThis rectangle defines the active (non-masked) pixels of the sensor. The order of the rectangle coordinates is: top, left, bottom, right.
0xc68e50830ImageExif.Image.MaskedAreasShortThis 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.
0xc68f50831ImageExif.Image.AsShotICCProfileUndefinedThis 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.
0xc69050832ImageExif.Image.AsShotPreProfileMatrixSRationalThis 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.
0xc69150833ImageExif.Image.CurrentICCProfileUndefinedThis 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.
0xc69250834ImageExif.Image.CurrentPreProfileMatrixSRationalThis 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.
0xc6bf50879ImageExif.Image.ColorimetricReferenceShortThe 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.
0xc6f350931ImageExif.Image.CameraCalibrationSignatureByteA 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.
0xc6f450932ImageExif.Image.ProfileCalibrationSignatureByteA 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.
0xc6f650934ImageExif.Image.AsShotProfileNameByteA UTF-8 encoded string containing the name of the "as shot" camera profile, if any.
0xc6f750935ImageExif.Image.NoiseReductionAppliedRationalThis 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.
0xc6f850936ImageExif.Image.ProfileNameByteA 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.
0xc6f950937ImageExif.Image.ProfileHueSatMapDimsLongThis 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.
0xc6fa50938ImageExif.Image.ProfileHueSatMapData1FloatThis 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.
0xc6fb50939ImageExif.Image.ProfileHueSatMapData2FloatThis 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.
0xc6fc50940ImageExif.Image.ProfileToneCurveFloatThis 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.
0xc6fd50941ImageExif.Image.ProfileEmbedPolicyLongThis tag contains information about the usage rules for the associated camera profile.
0xc6fe50942ImageExif.Image.ProfileCopyrightByteA 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.
0xc71450964ImageExif.Image.ForwardMatrix1SRationalThis tag defines a matrix that maps white balanced camera colors to XYZ D50 colors.
0xc71550965ImageExif.Image.ForwardMatrix2SRationalThis tag defines a matrix that maps white balanced camera colors to XYZ D50 colors.
0xc71650966ImageExif.Image.PreviewApplicationNameByteA UTF-8 encoded string containing the name of the application that created the preview stored in the IFD.
0xc71750967ImageExif.Image.PreviewApplicationVersionByteA UTF-8 encoded string containing the version number of the application that created the preview stored in the IFD.
0xc71850968ImageExif.Image.PreviewSettingsNameByteA UTF-8 encoded string containing the name of the conversion settings (for example, snapshot name) used for the preview stored in the IFD.
0xc71950969ImageExif.Image.PreviewSettingsDigestByteA unique ID of the conversion settings (for example, MD5 digest) used to render the preview stored in the IFD.
0xc71a50970ImageExif.Image.PreviewColorSpaceLongThis 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.
0xc71b50971ImageExif.Image.PreviewDateTimeAsciiThis 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.
0xc71c50972ImageExif.Image.RawImageDigestUndefinedThis 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.
0xc71d50973ImageExif.Image.OriginalRawFileDigestUndefinedThis tag is an MD5 digest of the data stored in the OriginalRawFileData tag.
0xc71e50974ImageExif.Image.SubTileBlockSizeLongNormally, 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.
0xc71f50975ImageExif.Image.RowInterleaveFactorLongThis 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.
0xc72550981ImageExif.Image.ProfileLookTableDimsLongThis 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.
0xc72650982ImageExif.Image.ProfileLookTableDataFloatThis 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.
0xc74051008ImageExif.Image.OpcodeList1UndefinedSpecifies the list of opcodes that should be applied to the raw image, as read directly from the file.
0xc74151009ImageExif.Image.OpcodeList2UndefinedSpecifies the list of opcodes that should be applied to the raw image, just after it has been mapped to linear reference values.
0xc74e51022ImageExif.Image.OpcodeList3UndefinedSpecifies the list of opcodes that should be applied to the raw image, just after it has been demosaiced.
0xc76151041ImageExif.Image.NoiseProfileDoubleNoiseProfile 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.).
0x829a33434PhotoExif.Photo.ExposureTimeRationalExposure time, given in seconds (sec).
0x829d33437PhotoExif.Photo.FNumberRationalThe F number.
0x882234850PhotoExif.Photo.ExposureProgramShortThe class of the program used by the camera to set exposure when the picture is taken.
0x882434852PhotoExif.Photo.SpectralSensitivityAsciiIndicates 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.
0x882734855PhotoExif.Photo.ISOSpeedRatingsShortIndicates the ISO Speed and ISO Latitude of the camera or input device as specified in ISO 12232.
0x882834856PhotoExif.Photo.OECFUndefinedIndicates the Opto-Electoric Conversion Function (OECF) specified in ISO 14524. <OECF> is the relationship between the camera optical input and the image values.
0x883034864PhotoExif.Photo.SensitivityTypeShortThe 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.
0x883134865PhotoExif.Photo.StandardOutputSensitivityLongThis 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.
0x883234866PhotoExif.Photo.RecommendedExposureIndexLongThis 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.
0x883334867PhotoExif.Photo.ISOSpeedLongThis 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.
0x883434868PhotoExif.Photo.ISOSpeedLatitudeyyyLongThis 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.
0x883534869PhotoExif.Photo.ISOSpeedLatitudezzzLongThis 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.
0x900036864PhotoExif.Photo.ExifVersionUndefinedThe version of this standard supported. Nonexistence of this field is taken to mean nonconformance to the standard.
0x900336867PhotoExif.Photo.DateTimeOriginalAsciiThe 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.
0x900436868PhotoExif.Photo.DateTimeDigitizedAsciiThe date and time when the image was stored as digital data.
0x910137121PhotoExif.Photo.ComponentsConfigurationUndefinedInformation 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 <PhotometricInterpretation> tag. However, since <PhotometricInterpretation> 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.
0x910237122PhotoExif.Photo.CompressedBitsPerPixelRationalInformation specific to compressed data. The compression mode used for a compressed image is indicated in unit bits per pixel.
0x920137377PhotoExif.Photo.ShutterSpeedValueSRationalShutter speed. The unit is the APEX (Additive System of Photographic Exposure) setting.
0x920237378PhotoExif.Photo.ApertureValueRationalThe lens aperture. The unit is the APEX value.
0x920337379PhotoExif.Photo.BrightnessValueSRationalThe value of brightness. The unit is the APEX value. Ordinarily it is given in the range of -99.99 to 99.99.
0x920437380PhotoExif.Photo.ExposureBiasValueSRationalThe exposure bias. The units is the APEX value. Ordinarily it is given in the range of -99.99 to 99.99.
0x920537381PhotoExif.Photo.MaxApertureValueRationalThe 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.
0x920637382PhotoExif.Photo.SubjectDistanceRationalThe distance to the subject, given in meters.
0x920737383PhotoExif.Photo.MeteringModeShortThe metering mode.
0x920837384PhotoExif.Photo.LightSourceShortThe kind of light source.
0x920937385PhotoExif.Photo.FlashShortThis tag is recorded when an image is taken using a strobe light (flash).
0x920a37386PhotoExif.Photo.FocalLengthRationalThe actual focal length of the lens, in mm. Conversion is not made to the focal length of a 35 mm film camera.
0x921437396PhotoExif.Photo.SubjectAreaShortThis tag indicates the location and area of the main subject in the overall scene.
0x927c37500PhotoExif.Photo.MakerNoteUndefinedA tag for manufacturers of Exif writers to record any desired information. The contents are up to the manufacturer.
0x928637510PhotoExif.Photo.UserCommentCommentA tag for Exif users to write keywords or comments on the image besides those in <ImageDescription>, and without the character code limitations of the <ImageDescription> tag.
0x929037520PhotoExif.Photo.SubSecTimeAsciiA tag used to record fractions of seconds for the <DateTime> tag.
0x929137521PhotoExif.Photo.SubSecTimeOriginalAsciiA tag used to record fractions of seconds for the <DateTimeOriginal> tag.
0x929237522PhotoExif.Photo.SubSecTimeDigitizedAsciiA tag used to record fractions of seconds for the <DateTimeDigitized> tag.
0xa00040960PhotoExif.Photo.FlashpixVersionUndefinedThe FlashPix format version supported by a FPXR file.
0xa00140961PhotoExif.Photo.ColorSpaceShortThe 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.
0xa00240962PhotoExif.Photo.PixelXDimensionLongInformation 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.
0xa00340963PhotoExif.Photo.PixelYDimensionLongInformation 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.
0xa00440964PhotoExif.Photo.RelatedSoundFileAsciiThis 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.
0xa00540965PhotoExif.Photo.InteroperabilityTagLongInteroperability 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.
0xa20b41483PhotoExif.Photo.FlashEnergyRationalIndicates the strobe energy at the time the image is captured, as measured in Beam Candle Power Seconds (BCPS).
0xa20c41484PhotoExif.Photo.SpatialFrequencyResponseUndefinedThis 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.
0xa20e41486PhotoExif.Photo.FocalPlaneXResolutionRationalIndicates the number of pixels in the image width (X) direction per <FocalPlaneResolutionUnit> on the camera focal plane.
0xa20f41487PhotoExif.Photo.FocalPlaneYResolutionRationalIndicates the number of pixels in the image height (V) direction per <FocalPlaneResolutionUnit> on the camera focal plane.
0xa21041488PhotoExif.Photo.FocalPlaneResolutionUnitShortIndicates the unit for measuring <FocalPlaneXResolution> and <FocalPlaneYResolution>. This value is the same as the <ResolutionUnit>.
0xa21441492PhotoExif.Photo.SubjectLocationShortIndicates 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 <Rotation> tag. The first value indicates the X column number and second indicates the Y row number.
0xa21541493PhotoExif.Photo.ExposureIndexRationalIndicates the exposure index selected on the camera or input device at the time the image is captured.
0xa21741495PhotoExif.Photo.SensingMethodShortIndicates the image sensor type on the camera or input device.
0xa30041728PhotoExif.Photo.FileSourceUndefinedIndicates 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.
0xa30141729PhotoExif.Photo.SceneTypeUndefinedIndicates 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.
0xa30241730PhotoExif.Photo.CFAPatternUndefinedIndicates 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.
0xa40141985PhotoExif.Photo.CustomRenderedShortThis 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.
0xa40241986PhotoExif.Photo.ExposureModeShortThis 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.
0xa40341987PhotoExif.Photo.WhiteBalanceShortThis tag indicates the white balance mode set when the image was shot.
0xa40441988PhotoExif.Photo.DigitalZoomRatioRationalThis 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.
0xa40541989PhotoExif.Photo.FocalLengthIn35mmFilmShortThis 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 <FocalLength> tag.
0xa40641990PhotoExif.Photo.SceneCaptureTypeShortThis 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 <SceneType> tag.
0xa40741991PhotoExif.Photo.GainControlShortThis tag indicates the degree of overall image gain adjustment.
0xa40841992PhotoExif.Photo.ContrastShortThis tag indicates the direction of contrast processing applied by the camera when the image was shot.
0xa40941993PhotoExif.Photo.SaturationShortThis tag indicates the direction of saturation processing applied by the camera when the image was shot.
0xa40a41994PhotoExif.Photo.SharpnessShortThis tag indicates the direction of sharpness processing applied by the camera when the image was shot.
0xa40b41995PhotoExif.Photo.DeviceSettingDescriptionUndefinedThis 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.
0xa40c41996PhotoExif.Photo.SubjectDistanceRangeShortThis tag indicates the distance to the subject.
0xa42042016PhotoExif.Photo.ImageUniqueIDAsciiThis 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.
0xa43042032PhotoExif.Photo.CameraOwnerNameAsciiThis tag records the owner of a camera used in photography as an ASCII string.
0xa43142033PhotoExif.Photo.BodySerialNumberAsciiThis tag records the serial number of the body of the camera that was used in photography as an ASCII string.
0xa43242034PhotoExif.Photo.LensSpecificationRationalThis 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
0xa43342035PhotoExif.Photo.LensMakeAsciiThis tag records the lens manufactor as an ASCII string.
0xa43442036PhotoExif.Photo.LensModelAsciiThis tag records the lens's model name and model number as an ASCII string.
0xa43542037PhotoExif.Photo.LensSerialNumberAsciiThis tag records the serial number of the interchangeable lens that was used in photography as an ASCII string.
0x00011IopExif.Iop.InteroperabilityIndexAsciiIndicates 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.
0x00022IopExif.Iop.InteroperabilityVersionUndefinedInteroperability version
0x10004096IopExif.Iop.RelatedImageFileFormatAsciiFile format of image file
0x10014097IopExif.Iop.RelatedImageWidthLongImage width
0x10024098IopExif.Iop.RelatedImageLengthLongImage height
0x00000GPSInfoExif.GPSInfo.GPSVersionIDByteIndicates the version of <GPSInfoIFD>. The version is given as 2.0.0.0. This tag is mandatory when <GPSInfo> tag is present. (Note: The <GPSVersionID> tag is given in bytes, unlike the <ExifVersion> tag. When the version is 2.0.0.0, the tag value is 02000000.H).
0x00011GPSInfoExif.GPSInfo.GPSLatitudeRefAsciiIndicates whether the latitude is north or south latitude. The ASCII value 'N' indicates north latitude, and 'S' is south latitude.
0x00022GPSInfoExif.GPSInfo.GPSLatitudeRationalIndicates 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.
0x00033GPSInfoExif.GPSInfo.GPSLongitudeRefAsciiIndicates whether the longitude is east or west longitude. ASCII 'E' indicates east longitude, and 'W' is west longitude.
0x00044GPSInfoExif.GPSInfo.GPSLongitudeRationalIndicates 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.
0x00055GPSInfoExif.GPSInfo.GPSAltitudeRefByteIndicates 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.
0x00066GPSInfoExif.GPSInfo.GPSAltitudeRationalIndicates the altitude based on the reference in GPSAltitudeRef. Altitude is expressed as one RATIONAL value. The reference unit is meters.
0x00077GPSInfoExif.GPSInfo.GPSTimeStampRationalIndicates the time as UTC (Coordinated Universal Time). <TimeStamp> is expressed as three RATIONAL values giving the hour, minute, and second (atomic clock).
0x00088GPSInfoExif.GPSInfo.GPSSatellitesAsciiIndicates 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.
0x00099GPSInfoExif.GPSInfo.GPSStatusAsciiIndicates the status of the GPS receiver when the image is recorded. "A" means measurement is in progress, and "V" means the measurement is Interoperability.
0x000a10GPSInfoExif.GPSInfo.GPSMeasureModeAsciiIndicates the GPS measurement mode. "2" means two-dimensional measurement and "3" means three-dimensional measurement is in progress.
0x000b11GPSInfoExif.GPSInfo.GPSDOPRationalIndicates the GPS DOP (data degree of precision). An HDOP value is written during two-dimensional measurement, and PDOP during three-dimensional measurement.
0x000c12GPSInfoExif.GPSInfo.GPSSpeedRefAsciiIndicates the unit used to express the GPS receiver speed of movement. "K" "M" and "N" represents kilometers per hour, miles per hour, and knots.
0x000d13GPSInfoExif.GPSInfo.GPSSpeedRationalIndicates the speed of GPS receiver movement.
0x000e14GPSInfoExif.GPSInfo.GPSTrackRefAsciiIndicates the reference for giving the direction of GPS receiver movement. "T" denotes true direction and "M" is magnetic direction.
0x000f15GPSInfoExif.GPSInfo.GPSTrackRationalIndicates the direction of GPS receiver movement. The range of values is from 0.00 to 359.99.
0x001016GPSInfoExif.GPSInfo.GPSImgDirectionRefAsciiIndicates the reference for giving the direction of the image when it is captured. "T" denotes true direction and "M" is magnetic direction.
0x001117GPSInfoExif.GPSInfo.GPSImgDirectionRationalIndicates the direction of the image when it was captured. The range of values is from 0.00 to 359.99.
0x001218GPSInfoExif.GPSInfo.GPSMapDatumAsciiIndicates 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".
0x001319GPSInfoExif.GPSInfo.GPSDestLatitudeRefAsciiIndicates whether the latitude of the destination point is north or south latitude. The ASCII value "N" indicates north latitude, and "S" is south latitude.
0x001420GPSInfoExif.GPSInfo.GPSDestLatitudeRationalIndicates 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.
0x001521GPSInfoExif.GPSInfo.GPSDestLongitudeRefAsciiIndicates whether the longitude of the destination point is east or west longitude. ASCII "E" indicates east longitude, and "W" is west longitude.
0x001622GPSInfoExif.GPSInfo.GPSDestLongitudeRationalIndicates 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.
0x001723GPSInfoExif.GPSInfo.GPSDestBearingRefAsciiIndicates the reference used for giving the bearing to the destination point. "T" denotes true direction and "M" is magnetic direction.
0x001824GPSInfoExif.GPSInfo.GPSDestBearingRationalIndicates the bearing to the destination point. The range of values is from 0.00 to 359.99.
0x001925GPSInfoExif.GPSInfo.GPSDestDistanceRefAsciiIndicates the unit used to express the distance to the destination point. "K", "M" and "N" represent kilometers, miles and knots.
0x001a26GPSInfoExif.GPSInfo.GPSDestDistanceRationalIndicates the distance to the destination point.
0x001b27GPSInfoExif.GPSInfo.GPSProcessingMethodUndefinedA 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.
0x001c28GPSInfoExif.GPSInfo.GPSAreaInformationUndefinedA 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.
0x001d29GPSInfoExif.GPSInfo.GPSDateStampAsciiA character string recording date and time information relative to UTC (Coordinated Universal Time). The format is "YYYY:MM:DD.".
0x001e30GPSInfoExif.GPSInfo.GPSDifferentialShortIndicates whether differential correction is applied to the GPS receiver.
diff --git a/assets/raw_tags/translate_tags.py b/assets/raw_tags/translate_tags.py new file mode 100644 index 0000000..77eba20 --- /dev/null +++ b/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/assets/tags.yaml b/assets/tags.yaml new file mode 100644 index 0000000..080c919 --- /dev/null +++ b/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/command/exif-read-tool/main.go b/command/exif-read-tool/main.go new file mode 100644 index 0000000..b972b70 --- /dev/null +++ b/command/exif-read-tool/main.go @@ -0,0 +1,173 @@ +// This tool dumps EXIF information from images. +// +// Example command-line: +// +// exif-read-tool -filepath +// +// Example Output: +// +// IFD=[IfdIdentity] ID=(0x010f) NAME=[Make] COUNT=(6) TYPE=[ASCII] VALUE=[Canon] +// IFD=[IfdIdentity] ID=(0x0110) NAME=[Model] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III] +// IFD=[IfdIdentity] ID=(0x0112) NAME=[Orientation] COUNT=(1) TYPE=[SHORT] VALUE=[1] +// IFD=[IfdIdentity] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1] +// ... +package main + +import ( + "fmt" + "os" + + "encoding/json" + "io/ioutil" + + "github.com/dsoprea/go-logging" + "github.com/jessevdk/go-flags" + + "b612.me/exif" + "b612.me/exif/common" +) + +const ( + thumbnailFilenameIndexPlaceholder = "" +) + +var ( + mainLogger = log.NewLogger("main.main") +) + +// IfdEntry is a JSON model for representing a single tag. +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 exifcommon.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"` +} + +type parameters struct { + Filepath string `short:"f" long:"filepath" required:"true" description:"File-path of image"` + PrintAsJson bool `short:"j" long:"json" description:"Print out as JSON"` + IsVerbose bool `short:"v" long:"verbose" description:"Print logging"` + ThumbnailOutputFilepath string `short:"t" long:"thumbnail-output-filepath" description:"File-path to write thumbnail to (if present)"` + DoNotPrintTags bool `short:"n" long:"no-tags" description:"Do not actually print tags. Good for auditing the logs or merely checking the EXIF structure for errors."` + SkipBlocks int `short:"s" long:"skip" description:"Skip this many EXIF blocks before returning"` + DoUniversalTagSearch bool `short:"u" long:"universal-tags" description:"If tags not found in known mapped IFDs, fallback to trying all IFDs."` +} + +var ( + arguments = new(parameters) +) + +func main() { + defer func() { + if errRaw := recover(); errRaw != nil { + err := errRaw.(error) + log.PrintError(err) + + os.Exit(-2) + } + }() + + _, err := flags.Parse(arguments) + if err != nil { + os.Exit(-1) + } + + if arguments.IsVerbose == true { + cla := log.NewConsoleLogAdapter() + log.AddAdapter("console", cla) + + scp := log.NewStaticConfigurationProvider() + scp.SetLevelName(log.LevelNameDebug) + + log.LoadConfiguration(scp) + } + + f, err := os.Open(arguments.Filepath) + log.PanicIf(err) + + data, err := ioutil.ReadAll(f) + log.PanicIf(err) + + rawExif, err := exif.SearchAndExtractExifN(data, arguments.SkipBlocks) + if err != nil { + if err == exif.ErrNoExif { + fmt.Printf("No EXIF data.\n") + os.Exit(1) + } + + log.Panic(err) + } + + mainLogger.Debugf(nil, "EXIF blob is (%d) bytes.", len(rawExif)) + + // Run the parse. + + entries, _, err := exif.GetFlatExifDataUniversalSearch(rawExif, nil, arguments.DoUniversalTagSearch) + if err != nil { + if arguments.SkipBlocks > 0 { + mainLogger.Warningf(nil, "Encountered an error. This might be related to the request to skip EXIF blocks.") + } + + log.Panic(err) + } + + // Write the thumbnail is requested and present. + + thumbnailOutputFilepath := arguments.ThumbnailOutputFilepath + if thumbnailOutputFilepath != "" { + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := exif.NewTagIndex() + + _, index, err := exif.Collect(im, ti, rawExif) + log.PanicIf(err) + + var thumbnail []byte + if ifd, found := index.Lookup[exif.ThumbnailFqIfdPath]; found == true { + thumbnail, err = ifd.Thumbnail() + if err != nil && err != exif.ErrNoThumbnail { + log.Panic(err) + } + } + + if thumbnail == nil { + mainLogger.Debugf(nil, "No thumbnails found.") + } else { + if arguments.PrintAsJson == false { + fmt.Printf("Writing (%d) bytes for thumbnail: [%s]\n", len(thumbnail), thumbnailOutputFilepath) + fmt.Printf("\n") + } + + err := ioutil.WriteFile(thumbnailOutputFilepath, thumbnail, 0644) + log.PanicIf(err) + } + } + + if arguments.DoNotPrintTags == false { + if arguments.PrintAsJson == true { + data, err := json.MarshalIndent(entries, "", " ") + log.PanicIf(err) + + fmt.Println(string(data)) + } else { + thumbnailTags := 0 + 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.Formatted) + } + + fmt.Printf("\n") + + if thumbnailTags == 2 { + fmt.Printf("There is a thumbnail.\n") + fmt.Printf("\n") + } + } + } +} diff --git a/command/exif-read-tool/main_test.go b/command/exif-read-tool/main_test.go new file mode 100644 index 0000000..8dcbd96 --- /dev/null +++ b/command/exif-read-tool/main_test.go @@ -0,0 +1,180 @@ +package main + +import ( + "bytes" + "fmt" + "path" + "reflect" + "testing" + + "encoding/json" + "io/ioutil" + "os/exec" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +func TestMainProc(t *testing.T) { + appFilepath := getAppFilepath() + testImageFilepath := getTestImageFilepath() + + 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=[Exif9101ComponentsConfiguration] +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] +IFD-PATH=[IFD/Exif] ID=(0x9286) NAME=[UserComment] COUNT=(264) TYPE=[UNDEFINED] VALUE=[UserComment] +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 35/1 0/1 0/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=[IFD1] ID=(0x0103) NAME=[Compression] COUNT=(1) TYPE=[SHORT] VALUE=[[6]] +IFD-PATH=[IFD1] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[[72/1]] +IFD-PATH=[IFD1] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[[72/1]] +IFD-PATH=[IFD1] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[[2]] +IFD-PATH=[IFD1] ID=(0x0201) NAME=[JPEGInterchangeFormat] COUNT=(1) TYPE=[LONG] VALUE=[[11444]] +IFD-PATH=[IFD1] ID=(0x0202) NAME=[JPEGInterchangeFormatLength] COUNT=(1) TYPE=[LONG] VALUE=[[21491]] + +` + + if actual != expected { + t.Fatalf("Output not as expected:\nACTUAL:\n%s\nEXPECTED:\n%s", actual, expected) + } +} + +func TestMainJson(t *testing.T) { + appFilepath := getAppFilepath() + testImageFilepath := getTestImageFilepath() + + 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. + + assetsPath := exifcommon.GetTestAssetsPath() + jsonFilepath := path.Join(assetsPath, "main_test_exif.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) + // } + + for i, tagInfo := range actual { + if reflect.DeepEqual(tagInfo, expected[i]) == false { + actualBytes, err := json.MarshalIndent(tagInfo, "", " ") + log.PanicIf(err) + + expectedBytes, err := json.MarshalIndent(expected[i], "", " ") + log.PanicIf(err) + + t.Fatalf("Tag (%d) not as expected:\nACTUAL:\n%s\nEXPECTED:\n%s", i, string(actualBytes), string(expectedBytes)) + } + } + + if len(actual) != len(expected) { + t.Fatalf("Actual tags not same length as expected tags.") + } +} + +func getAppFilepath() string { + moduleRootPath := exifcommon.GetModuleRootPath() + appFilepath := path.Join(moduleRootPath, "command", "exif-read-tool", "main.go") + + return appFilepath +} + +func getTestImageFilepath() string { + assetsPath := exifcommon.GetTestAssetsPath() + testImageFilepath := path.Join(assetsPath, "NDM_8901.jpg") + + return testImageFilepath +} diff --git a/common/assets/NDM_8901.jpg.exif b/common/assets/NDM_8901.jpg.exif new file mode 100644 index 0000000..58ec96f Binary files /dev/null and b/common/assets/NDM_8901.jpg.exif differ diff --git a/common/ifd.go b/common/ifd.go new file mode 100644 index 0000000..01886e9 --- /dev/null +++ b/common/ifd.go @@ -0,0 +1,651 @@ +package exifcommon + +import ( + "errors" + "fmt" + "strings" + + "github.com/dsoprea/go-logging" +) + +var ( + ifdLogger = log.NewLogger("exifcommon.ifd") +) + +var ( + ErrChildIfdNotMapped = errors.New("no child-IFD for that tag-ID under parent") +) + +// MappedIfd is one node in the IFD-mapping. +type MappedIfd struct { + ParentTagId uint16 + Placement []uint16 + Path []string + + Name string + TagId uint16 + Children map[uint16]*MappedIfd +} + +// String returns a descriptive string. +func (mi *MappedIfd) String() string { + pathPhrase := mi.PathPhrase() + return fmt.Sprintf("MappedIfd<(0x%04X) [%s] PATH=[%s]>", mi.TagId, mi.Name, pathPhrase) +} + +// PathPhrase returns a non-fully-qualified IFD path. +func (mi *MappedIfd) PathPhrase() string { + return strings.Join(mi.Path, "/") +} + +// TODO(dustin): Refactor this to use IfdIdentity structs. + +// IfdMapping describes all of the IFDs that we currently recognize. +type IfdMapping struct { + rootNode *MappedIfd +} + +// NewIfdMapping returns a new IfdMapping struct. +func NewIfdMapping() (ifdMapping *IfdMapping) { + rootNode := &MappedIfd{ + Path: make([]string, 0), + Children: make(map[uint16]*MappedIfd), + } + + return &IfdMapping{ + rootNode: rootNode, + } +} + +// NewIfdMappingWithStandard retruns a new IfdMapping struct preloaded with the +// standard IFDs. +func NewIfdMappingWithStandard() (ifdMapping *IfdMapping, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + im := NewIfdMapping() + + err = LoadStandardIfds(im) + log.PanicIf(err) + + return im, nil +} + +// Get returns the node given the path slice. +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 +} + +// GetWithPath returns the node given the path string. +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 +} + +// IfdTagIdAndIndex represents a specific part of the IFD path. +// +// This is a legacy type. +type IfdTagIdAndIndex struct { + Name string + TagId uint16 + Index int +} + +// String returns a descriptive string. +func (itii IfdTagIdAndIndex) String() string { + return fmt.Sprintf("IfdTagIdAndIndex", 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 +} + +// FqPathPhraseFromLineage returns the fully-qualified IFD path from the slice. +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, "/") +} + +// PathPhraseFromLineage returns the non-fully-qualified IFD path from the +// slice. +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 +} + +// DumpLineages returns a slice of strings representing all mappings. +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 +} + +// LoadStandardIfds loads the standard IFDs into the mapping. +func LoadStandardIfds(im *IfdMapping) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + err = im.Add( + []uint16{}, + IfdStandardIfdIdentity.TagId(), IfdStandardIfdIdentity.Name()) + + log.PanicIf(err) + + err = im.Add( + []uint16{IfdStandardIfdIdentity.TagId()}, + IfdExifStandardIfdIdentity.TagId(), IfdExifStandardIfdIdentity.Name()) + + log.PanicIf(err) + + err = im.Add( + []uint16{IfdStandardIfdIdentity.TagId(), IfdExifStandardIfdIdentity.TagId()}, + IfdExifIopStandardIfdIdentity.TagId(), IfdExifIopStandardIfdIdentity.Name()) + + log.PanicIf(err) + + err = im.Add( + []uint16{IfdStandardIfdIdentity.TagId()}, + IfdGpsInfoStandardIfdIdentity.TagId(), IfdGpsInfoStandardIfdIdentity.Name()) + + log.PanicIf(err) + + return nil +} + +// IfdTag describes a single IFD tag and its parent (if any). +type IfdTag struct { + parentIfdTag *IfdTag + tagId uint16 + name string +} + +func NewIfdTag(parentIfdTag *IfdTag, tagId uint16, name string) IfdTag { + return IfdTag{ + parentIfdTag: parentIfdTag, + tagId: tagId, + name: name, + } +} + +// ParentIfd returns the IfdTag of this IFD's parent. +func (it IfdTag) ParentIfd() *IfdTag { + return it.parentIfdTag +} + +// TagId returns the tag-ID of this IFD. +func (it IfdTag) TagId() uint16 { + return it.tagId +} + +// Name returns the simple name of this IFD. +func (it IfdTag) Name() string { + return it.name +} + +// String returns a descriptive string. +func (it IfdTag) String() string { + parentIfdPhrase := "" + if it.parentIfdTag != nil { + parentIfdPhrase = fmt.Sprintf(" PARENT=(0x%04x)[%s]", it.parentIfdTag.tagId, it.parentIfdTag.name) + } + + return fmt.Sprintf("IfdTag", it.tagId, it.name, parentIfdPhrase) +} + +var ( + // rootStandardIfd is the standard root IFD. + rootStandardIfd = NewIfdTag(nil, 0x0000, "IFD") // IFD + + // exifStandardIfd is the standard "Exif" IFD. + exifStandardIfd = NewIfdTag(&rootStandardIfd, 0x8769, "Exif") // IFD/Exif + + // iopStandardIfd is the standard "Iop" IFD. + iopStandardIfd = NewIfdTag(&exifStandardIfd, 0xA005, "Iop") // IFD/Exif/Iop + + // gpsInfoStandardIfd is the standard "GPS" IFD. + gpsInfoStandardIfd = NewIfdTag(&rootStandardIfd, 0x8825, "GPSInfo") // IFD/GPSInfo +) + +// IfdIdentityPart represents one component in an IFD path. +type IfdIdentityPart struct { + Name string + Index int +} + +// String returns a fully-qualified IFD path. +func (iip IfdIdentityPart) String() string { + if iip.Index > 0 { + return fmt.Sprintf("%s%d", iip.Name, iip.Index) + } else { + return iip.Name + } +} + +// UnindexedString returned a non-fully-qualified IFD path. +func (iip IfdIdentityPart) UnindexedString() string { + return iip.Name +} + +// IfdIdentity represents a single IFD path and provides access to various +// information and representations. +// +// Only global instances can be used for equality checks. +type IfdIdentity struct { + ifdTag IfdTag + parts []IfdIdentityPart + ifdPath string + fqIfdPath string +} + +// NewIfdIdentity returns a new IfdIdentity struct. +func NewIfdIdentity(ifdTag IfdTag, parts ...IfdIdentityPart) (ii *IfdIdentity) { + ii = &IfdIdentity{ + ifdTag: ifdTag, + parts: parts, + } + + ii.ifdPath = ii.getIfdPath() + ii.fqIfdPath = ii.getFqIfdPath() + + return ii +} + +// NewIfdIdentityFromString parses a string like "IFD/Exif" or "IFD1" or +// something more exotic with custom IFDs ("SomeIFD4/SomeChildIFD6"). Note that +// this will valid the unindexed IFD structure (because the standard tags from +// the specification are unindexed), but not, obviously, any indices (e.g. +// the numbers in "IFD0", "IFD1", "SomeIFD4/SomeChildIFD6"). It is +// required for the caller to check whether these specific instances +// were actually parsed out of the stream. +func NewIfdIdentityFromString(im *IfdMapping, fqIfdPath string) (ii *IfdIdentity, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + lineage, err := im.ResolvePath(fqIfdPath) + log.PanicIf(err) + + var lastIt *IfdTag + identityParts := make([]IfdIdentityPart, len(lineage)) + for i, itii := range lineage { + // Build out the tag that will eventually point to the IFD represented + // by the right-most part in the IFD path. + + it := &IfdTag{ + parentIfdTag: lastIt, + tagId: itii.TagId, + name: itii.Name, + } + + lastIt = it + + // Create the next IfdIdentity part. + + iip := IfdIdentityPart{ + Name: itii.Name, + Index: itii.Index, + } + + identityParts[i] = iip + } + + ii = NewIfdIdentity(*lastIt, identityParts...) + return ii, nil +} + +func (ii *IfdIdentity) getFqIfdPath() string { + partPhrases := make([]string, len(ii.parts)) + for i, iip := range ii.parts { + partPhrases[i] = iip.String() + } + + return strings.Join(partPhrases, "/") +} + +func (ii *IfdIdentity) getIfdPath() string { + partPhrases := make([]string, len(ii.parts)) + for i, iip := range ii.parts { + partPhrases[i] = iip.UnindexedString() + } + + return strings.Join(partPhrases, "/") +} + +// String returns a fully-qualified IFD path. +func (ii *IfdIdentity) String() string { + return ii.fqIfdPath +} + +// UnindexedString returns a non-fully-qualified IFD path. +func (ii *IfdIdentity) UnindexedString() string { + return ii.ifdPath +} + +// IfdTag returns the tag struct behind this IFD. +func (ii *IfdIdentity) IfdTag() IfdTag { + return ii.ifdTag +} + +// TagId returns the tag-ID of the IFD. +func (ii *IfdIdentity) TagId() uint16 { + return ii.ifdTag.TagId() +} + +// LeafPathPart returns the last right-most path-part, which represents the +// current IFD. +func (ii *IfdIdentity) LeafPathPart() IfdIdentityPart { + return ii.parts[len(ii.parts)-1] +} + +// Name returns the simple name of this IFD. +func (ii *IfdIdentity) Name() string { + return ii.LeafPathPart().Name +} + +// Index returns the index of this IFD (more then one IFD under a parent IFD +// will be numbered [0..n]). +func (ii *IfdIdentity) Index() int { + return ii.LeafPathPart().Index +} + +// Equals returns true if the two IfdIdentity instances are effectively +// identical. +// +// Since there's no way to get a specific fully-qualified IFD path without a +// certain slice of parts and all other fields are also derived from this, +// checking that the fully-qualified IFD path is equals is sufficient. +func (ii *IfdIdentity) Equals(ii2 *IfdIdentity) bool { + return ii.String() == ii2.String() +} + +// NewChild creates an IfdIdentity for an IFD that is a child of the current +// IFD. +func (ii *IfdIdentity) NewChild(childIfdTag IfdTag, index int) (iiChild *IfdIdentity) { + if *childIfdTag.parentIfdTag != ii.ifdTag { + log.Panicf("can not add child; we are not the parent:\nUS=%v\nCHILD=%v", ii.ifdTag, childIfdTag) + } + + childPart := IfdIdentityPart{childIfdTag.name, index} + childParts := append(ii.parts, childPart) + + iiChild = NewIfdIdentity(childIfdTag, childParts...) + return iiChild +} + +// NewSibling creates an IfdIdentity for an IFD that is a sibling to the current +// one. +func (ii *IfdIdentity) NewSibling(index int) (iiSibling *IfdIdentity) { + parts := make([]IfdIdentityPart, len(ii.parts)) + + copy(parts, ii.parts) + parts[len(parts)-1].Index = index + + iiSibling = NewIfdIdentity(ii.ifdTag, parts...) + return iiSibling +} + +var ( + // IfdStandardIfdIdentity represents the IFD path for IFD0. + IfdStandardIfdIdentity = NewIfdIdentity(rootStandardIfd, IfdIdentityPart{"IFD", 0}) + + // IfdExifStandardIfdIdentity represents the IFD path for IFD0/Exif0. + IfdExifStandardIfdIdentity = IfdStandardIfdIdentity.NewChild(exifStandardIfd, 0) + + // IfdExifIopStandardIfdIdentity represents the IFD path for IFD0/Exif0/Iop0. + IfdExifIopStandardIfdIdentity = IfdExifStandardIfdIdentity.NewChild(iopStandardIfd, 0) + + // IfdGPSInfoStandardIfdIdentity represents the IFD path for IFD0/GPSInfo0. + IfdGpsInfoStandardIfdIdentity = IfdStandardIfdIdentity.NewChild(gpsInfoStandardIfd, 0) + + // Ifd1StandardIfdIdentity represents the IFD path for IFD1. + Ifd1StandardIfdIdentity = NewIfdIdentity(rootStandardIfd, IfdIdentityPart{"IFD", 1}) +) diff --git a/common/ifd_test.go b/common/ifd_test.go new file mode 100644 index 0000000..d3e7342 --- /dev/null +++ b/common/ifd_test.go @@ -0,0 +1,336 @@ +package exifcommon + +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{ + IfdStandardIfdIdentity.TagId(), + IfdExifStandardIfdIdentity.TagId(), + IfdExifIopStandardIfdIdentity.TagId(), + }) + + log.PanicIf(err) + + if mi.ParentTagId != IfdExifStandardIfdIdentity.TagId() { + t.Fatalf("Parent tag-ID not correct") + } else if mi.TagId != IfdExifIopStandardIfdIdentity.TagId() { + 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 != IfdExifStandardIfdIdentity.TagId() { + t.Fatalf("Parent tag-ID not correct") + } else if mi.TagId != IfdExifIopStandardIfdIdentity.TagId() { + 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{ + {Name: "IFD", TagId: 0, Index: 0}, + {Name: "Exif", TagId: 0x8769, Index: 0}, + {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{ + {Name: "IFD", TagId: 0, Index: 0}, + {Name: "Exif", TagId: 0x8769, Index: 1}, + {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{ + {Name: "IFD", Index: 0}, + {Name: "Exif", Index: 1}, + {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{ + {Name: "IFD", Index: 0}, + {Name: "Exif", Index: 1}, + {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, err := NewIfdMappingWithStandard() + log.PanicIf(err) + + imWithout := NewIfdMapping() + + err = LoadStandardIfds(imWithout) + log.PanicIf(err) + + 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.") + } +} + +func TestNewIfdIdentityFromString_Valid_WithoutIndexes(t *testing.T) { + im := NewIfdMapping() + + err := LoadStandardIfds(im) + log.PanicIf(err) + + fqIfdPath := "IFD/Exif" + + ii, err := NewIfdIdentityFromString(im, fqIfdPath) + log.PanicIf(err) + + if ii.String() != fqIfdPath { + t.Fatalf("'%s' IFD-path was not parsed correctly: [%s]", fqIfdPath, ii.String()) + } +} + +func TestNewIfdIdentityFromString_Valid_WithIndexes(t *testing.T) { + im := NewIfdMapping() + + err := LoadStandardIfds(im) + log.PanicIf(err) + + fqIfdPath := "IFD2/Exif4" + + ii, err := NewIfdIdentityFromString(im, fqIfdPath) + log.PanicIf(err) + + if ii.String() != fqIfdPath { + t.Fatalf("'%s' IFD-path was not parsed correctly: [%s]", fqIfdPath, ii.String()) + } +} + +func TestNewIfdIdentityFromString_Invalid_IfdPathJustRoot(t *testing.T) { + im := NewIfdMapping() + + err := LoadStandardIfds(im) + log.PanicIf(err) + + fqIfdPath := "XYZ" + + _, err = NewIfdIdentityFromString(im, fqIfdPath) + if err == nil { + t.Fatalf("Expected error from invalid path.") + } else if err.Error() != "ifd child with name [XYZ] not registered: [XYZ]" { + log.Panic(err) + } +} + +func TestNewIfdIdentityFromString_Invalid_IfdPathWithSubdirectory(t *testing.T) { + im := NewIfdMapping() + + err := LoadStandardIfds(im) + log.PanicIf(err) + + fqIfdPath := "IFD/XYZ" + + _, err = NewIfdIdentityFromString(im, fqIfdPath) + if err == nil { + t.Fatalf("Expected error from invalid path.") + } else if err.Error() != "ifd child with name [XYZ] not registered: [IFD/XYZ]" { + log.Panic(err) + } +} diff --git a/common/parser.go b/common/parser.go new file mode 100644 index 0000000..76e8ef4 --- /dev/null +++ b/common/parser.go @@ -0,0 +1,280 @@ +package exifcommon + +import ( + "bytes" + "errors" + "math" + + "encoding/binary" + + "github.com/dsoprea/go-logging" +) + +var ( + parserLogger = log.NewLogger("exifcommon.parser") +) + +var ( + ErrParseFail = errors.New("parse failure") +) + +// Parser knows how to parse all well-defined, encoded EXIF types. +type Parser struct { +} + +// ParseBytesknows how to parse a byte-type value. +func (p *Parser) ParseBytes(data []byte, unitCount uint32) (value []uint8, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test + + 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 that +// should be at the end of the encoding. +func (p *Parser) ParseAscii(data []byte, unitCount uint32) (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test + + count := int(unitCount) + + if len(data) < (TypeAscii.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + if len(data) == 0 || data[count-1] != 0 { + s := string(data[:count]) + parserLogger.Warningf(nil, "ASCII not terminated with NUL as expected: [%v]", s) + + for i, c := range s { + if c > 127 { + // Binary + + t := s[:i] + parserLogger.Warningf(nil, "ASCII also had binary characters. Truncating: [%v]->[%s]", s, t) + + return t, nil + } + } + + return s, nil + } + + // 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)) + } + }() + + // TODO(dustin): Add test + + count := int(unitCount) + + if len(data) < (TypeAscii.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + return string(data[:count]), nil +} + +// ParseShorts knows how to parse an encoded list of shorts. +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)) + } + }() + + // TODO(dustin): Add test + + 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 +} + +// ParseLongs knows how to encode an encoded list of unsigned longs. +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)) + } + }() + + // TODO(dustin): Add test + + 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 +} + +// ParseFloats knows how to encode an encoded list of floats. +func (p *Parser) ParseFloats(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []float32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + count := int(unitCount) + + if len(data) != (TypeFloat.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + value = make([]float32, count) + for i := 0; i < count; i++ { + value[i] = math.Float32frombits(byteOrder.Uint32(data[i*4 : (i+1)*4])) + } + + return value, nil +} + +// ParseDoubles knows how to encode an encoded list of doubles. +func (p *Parser) ParseDoubles(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []float64, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + count := int(unitCount) + + if len(data) != (TypeDouble.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + value = make([]float64, count) + for i := 0; i < count; i++ { + value[i] = math.Float64frombits(byteOrder.Uint64(data[i*8 : (i+1)*8])) + } + + return value, nil +} + +// ParseRationals knows how to parse an encoded list of unsigned rationals. +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)) + } + }() + + // TODO(dustin): Add test + + 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 +} + +// ParseSignedLongs knows how to parse an encoded list of signed longs. +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)) + } + }() + + // TODO(dustin): Add test + + 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 +} + +// ParseSignedRationals knows how to parse an encoded list of signed +// rationals. +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)) + } + }() + + // TODO(dustin): Add test + + 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/common/parser_test.go b/common/parser_test.go new file mode 100644 index 0000000..293c5db --- /dev/null +++ b/common/parser_test.go @@ -0,0 +1,393 @@ +package exifcommon + +import ( + "bytes" + "math" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" +) + +func TestParser_ParseBytes(t *testing.T) { + p := new(Parser) + + encoded := []byte("abcdefg") + + value, err := p.ParseBytes(encoded, 1) + log.PanicIf(err) + + if bytes.Equal(value, encoded[:1]) != true { + t.Fatalf("Encoding not correct (1): %v", value) + } + + value, err = p.ParseBytes(encoded, 4) + log.PanicIf(err) + + if bytes.Equal(value, encoded[:4]) != true { + t.Fatalf("Encoding not correct (2): %v", value) + } + + value, err = p.ParseBytes(encoded, uint32(len(encoded))) + log.PanicIf(err) + + if bytes.Equal(value, encoded) != true { + t.Fatalf("Encoding not correct (3): %v", value) + } +} + +func TestParser_ParseAscii(t *testing.T) { + p := new(Parser) + + original := "abcdefg" + + encoded := []byte(original[:1]) + encoded = append(encoded, 0) + + value, err := p.ParseAscii(encoded, uint32(len(encoded))) + log.PanicIf(err) + + if value != original[:1] { + t.Fatalf("Encoding not correct (1): %s", value) + } + + encoded = []byte(original[:4]) + encoded = append(encoded, 0) + + value, err = p.ParseAscii(encoded, uint32(len(encoded))) + log.PanicIf(err) + + if value != original[:4] { + t.Fatalf("Encoding not correct (2): %v", value) + } + + encoded = []byte(original) + encoded = append(encoded, 0) + + value, err = p.ParseAscii(encoded, uint32(len(encoded))) + log.PanicIf(err) + + if value != original { + t.Fatalf("Encoding not correct (3): %v", value) + } +} + +func TestParser_ParseAsciiNoNul(t *testing.T) { + p := new(Parser) + + original := "abcdefg" + + encoded := []byte(original) + + value, err := p.ParseAsciiNoNul(encoded, 1) + log.PanicIf(err) + + if value != original[:1] { + t.Fatalf("Encoding not correct (1): %s", value) + } + + value, err = p.ParseAsciiNoNul(encoded, 4) + log.PanicIf(err) + + if value != original[:4] { + t.Fatalf("Encoding not correct (2): %v", value) + } + + value, err = p.ParseAsciiNoNul(encoded, uint32(len(encoded))) + log.PanicIf(err) + + if value != original { + t.Fatalf("Encoding not correct (3): (%d) %v", len(value), value) + } +} + +func TestParser_ParseShorts__Single(t *testing.T) { + p := new(Parser) + + encoded := []byte{0x00, 0x01} + + value, err := p.ParseShorts(encoded, 1, TestDefaultByteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(value, []uint16{1}) != true { + t.Fatalf("Encoding not correct (1): %v", value) + } + + encoded = []byte{0x00, 0x01, 0x00, 0x02} + + value, err = p.ParseShorts(encoded, 1, TestDefaultByteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(value, []uint16{1}) != true { + t.Fatalf("Encoding not correct (2): %v", value) + } +} + +func TestParser_ParseShorts__Multiple(t *testing.T) { + p := new(Parser) + + encoded := []byte{0x00, 0x01, 0x00, 0x02} + + value, err := p.ParseShorts(encoded, 2, TestDefaultByteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(value, []uint16{1, 2}) != true { + t.Fatalf("Encoding not correct: %v", value) + } +} + +func TestParser_ParseLongs__Single(t *testing.T) { + p := new(Parser) + + encoded := []byte{0x00, 0x00, 0x00, 0x01} + + value, err := p.ParseLongs(encoded, 1, TestDefaultByteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(value, []uint32{1}) != true { + t.Fatalf("Encoding not correct (1): %v", value) + } + + encoded = []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02} + + value, err = p.ParseLongs(encoded, 1, TestDefaultByteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(value, []uint32{1}) != true { + t.Fatalf("Encoding not correct (2): %v", value) + } +} + +func TestParser_ParseLongs__Multiple(t *testing.T) { + p := new(Parser) + + encoded := []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02} + + value, err := p.ParseLongs(encoded, 2, TestDefaultByteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(value, []uint32{1, 2}) != true { + t.Fatalf("Encoding not correct: %v", value) + } +} + +func TestParser_ParseFloats__Single(t *testing.T) { + p := new(Parser) + + encoded := []byte{0x40, 0x49, 0x0f, 0xdb} + + value, err := p.ParseFloats(encoded, 1, TestDefaultByteOrder) + log.PanicIf(err) + + expectedResult := []float32{3.14159265} + + for i, v := range value { + if v < expectedResult[i] || + v >= math.Nextafter32(expectedResult[i], expectedResult[i]+1) { + t.Fatalf("Encoding not correct (1): %v", value) + } + } +} + +func TestParser_ParseFloats__Multiple(t *testing.T) { + p := new(Parser) + + encoded := []byte{0x40, 0x49, 0x0f, 0xdb, 0x40, 0x2d, 0xf8, 0x54} + + value, err := p.ParseFloats(encoded, 2, TestDefaultByteOrder) + log.PanicIf(err) + + expectedResult := []float32{3.14159265, 2.71828182} + + for i, v := range value { + if v < expectedResult[i] || + v >= math.Nextafter32(expectedResult[i], expectedResult[i]+1) { + t.Fatalf("Encoding not correct (1): %v", value) + } + } +} + +func TestParser_ParseDoubles__Single(t *testing.T) { + p := new(Parser) + + encoded := []byte{0x40, 0x09, 0x21, 0xfb, 0x53, 0xc8, 0xd4, 0xf1} + + value, err := p.ParseDoubles(encoded, 1, TestDefaultByteOrder) + log.PanicIf(err) + + expectedResult := []float64{3.14159265} + for i, v := range value { + if v < expectedResult[i] || + v >= math.Nextafter(expectedResult[i], expectedResult[i]+1) { + t.Fatalf("Encoding not correct (1): %v", value) + } + } +} + +func TestParser_ParseDoubles__Multiple(t *testing.T) { + p := new(Parser) + + encoded := []byte{0x40, 0x09, 0x21, 0xfb, 0x53, 0xc8, 0xd4, 0xf1, + 0x40, 0x05, 0xbf, 0x0a, 0x89, 0xf1, 0xb0, 0xdd} + + value, err := p.ParseDoubles(encoded, 2, TestDefaultByteOrder) + log.PanicIf(err) + + expectedResult := []float64{3.14159265, 2.71828182} + + for i, v := range value { + if v < expectedResult[i] || + v >= math.Nextafter(expectedResult[i], expectedResult[i]+1) { + t.Fatalf("Encoding not correct: %v", value) + } + } +} + +func TestParser_ParseRationals__Single(t *testing.T) { + p := new(Parser) + + encoded := []byte{ + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, + } + + value, err := p.ParseRationals(encoded, 1, TestDefaultByteOrder) + log.PanicIf(err) + + expected := []Rational{ + {Numerator: 1, Denominator: 2}, + } + + if reflect.DeepEqual(value, expected) != true { + t.Fatalf("Encoding not correct (1): %v", value) + } + + encoded = []byte{ + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, + } + + value, err = p.ParseRationals(encoded, 1, TestDefaultByteOrder) + log.PanicIf(err) + + expected = []Rational{ + {Numerator: 1, Denominator: 2}, + } + + if reflect.DeepEqual(value, expected) != true { + t.Fatalf("Encoding not correct (2): %v", value) + } +} + +func TestParser_ParseRationals__Multiple(t *testing.T) { + p := new(Parser) + + encoded := []byte{ + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, + } + + value, err := p.ParseRationals(encoded, 2, TestDefaultByteOrder) + log.PanicIf(err) + + expected := []Rational{ + {Numerator: 1, Denominator: 2}, + {Numerator: 3, Denominator: 4}, + } + + if reflect.DeepEqual(value, expected) != true { + t.Fatalf("Encoding not correct (2): %v", value) + } +} + +func TestParser_ParseSignedLongs__Single(t *testing.T) { + p := new(Parser) + + encoded := []byte{0x00, 0x00, 0x00, 0x01} + + value, err := p.ParseSignedLongs(encoded, 1, TestDefaultByteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(value, []int32{1}) != true { + t.Fatalf("Encoding not correct (1): %v", value) + } + + encoded = []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02} + + value, err = p.ParseSignedLongs(encoded, 1, TestDefaultByteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(value, []int32{1}) != true { + t.Fatalf("Encoding not correct (2): %v", value) + } +} + +func TestParser_ParseSignedLongs__Multiple(t *testing.T) { + p := new(Parser) + + encoded := []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02} + + value, err := p.ParseSignedLongs(encoded, 2, TestDefaultByteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(value, []int32{1, 2}) != true { + t.Fatalf("Encoding not correct: %v", value) + } +} + +func TestParser_ParseSignedRationals__Single(t *testing.T) { + p := new(Parser) + + encoded := []byte{ + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, + } + + value, err := p.ParseSignedRationals(encoded, 1, TestDefaultByteOrder) + log.PanicIf(err) + + expected := []SignedRational{ + {Numerator: 1, Denominator: 2}, + } + + if reflect.DeepEqual(value, expected) != true { + t.Fatalf("Encoding not correct (1): %v", value) + } + + encoded = []byte{ + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, + } + + value, err = p.ParseSignedRationals(encoded, 1, TestDefaultByteOrder) + log.PanicIf(err) + + expected = []SignedRational{ + {Numerator: 1, Denominator: 2}, + } + + if reflect.DeepEqual(value, expected) != true { + t.Fatalf("Encoding not correct (2): %v", value) + } +} + +func TestParser_ParseSignedRationals__Multiple(t *testing.T) { + p := new(Parser) + + encoded := []byte{ + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, + } + + value, err := p.ParseSignedRationals(encoded, 2, TestDefaultByteOrder) + log.PanicIf(err) + + expected := []SignedRational{ + {Numerator: 1, Denominator: 2}, + {Numerator: 3, Denominator: 4}, + } + + if reflect.DeepEqual(value, expected) != true { + t.Fatalf("Encoding not correct (2): %v", value) + } +} diff --git a/common/testing_common.go b/common/testing_common.go new file mode 100644 index 0000000..f04fa22 --- /dev/null +++ b/common/testing_common.go @@ -0,0 +1,88 @@ +package exifcommon + +import ( + "os" + "path" + + "encoding/binary" + "io/ioutil" + + "github.com/dsoprea/go-logging" +) + +var ( + moduleRootPath = "" + + testExifData []byte = nil + + // EncodeDefaultByteOrder is the default byte-order for encoding operations. + EncodeDefaultByteOrder = binary.BigEndian + + // Default byte order for tests. + TestDefaultByteOrder = binary.BigEndian +) + +func GetModuleRootPath() string { + if moduleRootPath == "" { + moduleRootPath = os.Getenv("EXIF_MODULE_ROOT_PATH") + if moduleRootPath != "" { + return moduleRootPath + } + + currentWd, err := os.Getwd() + log.PanicIf(err) + + currentPath := currentWd + + visited := make([]string, 0) + + for { + tryStampFilepath := path.Join(currentPath, ".MODULE_ROOT") + + _, err := os.Stat(tryStampFilepath) + if err != nil && os.IsNotExist(err) != true { + log.Panic(err) + } else if err == nil { + break + } + + visited = append(visited, tryStampFilepath) + + currentPath = path.Dir(currentPath) + if currentPath == "/" { + log.Panicf("could not find module-root: %v", visited) + } + } + + moduleRootPath = currentPath + } + + return moduleRootPath +} + +func GetTestAssetsPath() string { + moduleRootPath := GetModuleRootPath() + assetsPath := path.Join(moduleRootPath, "assets") + + return assetsPath +} + +func getTestImageFilepath() string { + assetsPath := GetTestAssetsPath() + testImageFilepath := path.Join(assetsPath, "NDM_8901.jpg") + return testImageFilepath +} + +func getTestExifData() []byte { + if testExifData == nil { + assetsPath := GetTestAssetsPath() + filepath := path.Join(assetsPath, "NDM_8901.jpg.exif") + + var err error + + testExifData, err = ioutil.ReadFile(filepath) + log.PanicIf(err) + } + + return testExifData +} diff --git a/common/type.go b/common/type.go new file mode 100644 index 0000000..e79bcb9 --- /dev/null +++ b/common/type.go @@ -0,0 +1,482 @@ +package exifcommon + +import ( + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "unicode" + + "encoding/binary" + + "github.com/dsoprea/go-logging" +) + +var ( + typeLogger = log.NewLogger("exif.type") +) + +var ( + // ErrNotEnoughData is used when there isn't enough data to accommodate 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") + + // ErrUnhandledUndefinedTypedTag 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). + ErrUnhandledUndefinedTypedTag = errors.New("not a standard unknown-typed tag") +) + +// TagTypePrimitive is a type-alias that let's us easily lookup type properties. +type TagTypePrimitive uint16 + +const ( + // TypeByte describes an encoded list of bytes. + TypeByte TagTypePrimitive = 1 + + // TypeAscii describes an encoded list of characters that is terminated + // with a NUL in its encoded form. + TypeAscii TagTypePrimitive = 2 + + // TypeShort describes an encoded list of shorts. + TypeShort TagTypePrimitive = 3 + + // TypeLong describes an encoded list of longs. + TypeLong TagTypePrimitive = 4 + + // TypeRational describes an encoded list of rationals. + TypeRational TagTypePrimitive = 5 + + // TypeUndefined describes an encoded value that has a complex/non-clearcut + // interpretation. + TypeUndefined TagTypePrimitive = 7 + + // We've seen type-8, but have no documentation on it. + + // TypeSignedLong describes an encoded list of signed longs. + TypeSignedLong TagTypePrimitive = 9 + + // TypeSignedRational describes an encoded list of signed rationals. + TypeSignedRational TagTypePrimitive = 10 + + // TypeFloat describes an encoded list of floats + TypeFloat TagTypePrimitive = 11 + + // TypeDouble describes an encoded list of doubles. + TypeDouble TagTypePrimitive = 12 + + // TypeAsciiNoNul is just a pseudo-type, for our own purposes. + TypeAsciiNoNul TagTypePrimitive = 0xf0 +) + +// String returns the name of the type +func (typeType TagTypePrimitive) String() string { + return TypeNames[typeType] +} + +// Size returns the size of one atomic unit of the type. +func (tagType TagTypePrimitive) Size() int { + switch tagType { + case TypeByte, TypeAscii, TypeAsciiNoNul: + return 1 + case TypeShort: + return 2 + case TypeLong, TypeSignedLong, TypeFloat: + return 4 + case TypeRational, TypeSignedRational, TypeDouble: + return 8 + default: + log.Panicf("can not determine tag-value size for type (%d): [%s]", + tagType, + TypeNames[tagType]) + // Never called. + return 0 + } +} + +// IsValid returns true if tagType is a valid type. +func (tagType TagTypePrimitive) IsValid() bool { + + // TODO(dustin): Add test + + return tagType == TypeByte || + tagType == TypeAscii || + tagType == TypeAsciiNoNul || + tagType == TypeShort || + tagType == TypeLong || + tagType == TypeRational || + tagType == TypeSignedLong || + tagType == TypeSignedRational || + tagType == TypeFloat || + tagType == TypeDouble || + tagType == TypeUndefined +} + +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", + TypeFloat: "FLOAT", + TypeDouble: "DOUBLE", + + TypeAsciiNoNul: "_ASCII_NO_NUL", + } + + typeNamesR = map[string]TagTypePrimitive{} +) + +// Rational describes an unsigned rational value. +type Rational struct { + // Numerator is the numerator of the rational value. + Numerator uint32 + + // Denominator is the numerator of the rational value. + Denominator uint32 +} + +// SignedRational describes a signed rational value. +type SignedRational struct { + // Numerator is the numerator of the rational value. + Numerator int32 + + // Denominator is the numerator of the rational value. + Denominator int32 +} + +func isPrintableText(s string) bool { + for _, c := range s { + // unicode.IsPrint() returns false for newline characters. + if c == 0x0d || c == 0x0a { + continue + } else if unicode.IsPrint(rune(c)) == false { + return false + } + } + + return true +} + +// Format returns a stringified value for the given encoding. Automatically +// parses. Automatically calculates count based on type size. This function +// also supports undefined-type values (the ones that we support, anyway) by +// way of the String() method that they all require. We can't be more specific +// because we're a base package and we can't refer to it. +func FormatFromType(value interface{}, justFirst bool) (phrase string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): !! Add test + + switch t := value.(type) { + case []byte: + return DumpBytesToString(t), nil + case string: + for i, c := range t { + if c == 0 { + t = t[:i] + break + } + } + + if isPrintableText(t) == false { + phrase = fmt.Sprintf("string with binary data (%d bytes)", len(t)) + return phrase, nil + } + + return t, nil + case []uint16, []uint32, []int32, []float64, []float32: + val := reflect.ValueOf(t) + + if val.Len() == 0 { + return "", nil + } + + if justFirst == true { + var valueSuffix string + if val.Len() > 1 { + valueSuffix = "..." + } + + return fmt.Sprintf("%v%s", val.Index(0), valueSuffix), nil + } + + return fmt.Sprintf("%v", val), nil + case []Rational: + if len(t) == 0 { + return "", nil + } + + parts := make([]string, len(t)) + for i, r := range t { + parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator) + + if justFirst == true { + break + } + } + + if justFirst == true { + var valueSuffix string + if len(t) > 1 { + valueSuffix = "..." + } + + return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil + } + + return fmt.Sprintf("%v", parts), nil + case []SignedRational: + if len(t) == 0 { + return "", nil + } + + parts := make([]string, len(t)) + for i, r := range t { + parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator) + + if justFirst == true { + break + } + } + + if justFirst == true { + var valueSuffix string + if len(t) > 1 { + valueSuffix = "..." + } + + return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil + } + + return fmt.Sprintf("%v", parts), nil + case fmt.Stringer: + s := t.String() + if isPrintableText(s) == false { + phrase = fmt.Sprintf("stringable with binary data (%d bytes)", len(s)) + return phrase, nil + } + + // An undefined value that is documented (or that we otherwise support). + return s, nil + default: + // Affects only "unknown" values, in general. + log.Panicf("type can not be formatted into string: %v", reflect.TypeOf(value).Name()) + + // Never called. + return "", nil + } +} + +// Format returns a stringified value for the given encoding. Automatically +// parses. Automatically calculates count based on type size. +func FormatFromBytes(rawBytes []byte, tagType TagTypePrimitive, justFirst bool, byteOrder binary.ByteOrder) (phrase string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): !! Add test + + 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. + + var value interface{} + + switch tagType { + case TypeByte: + var err error + + value, err = parser.ParseBytes(rawBytes, unitCount) + log.PanicIf(err) + case TypeAscii: + var err error + + value, err = parser.ParseAscii(rawBytes, unitCount) + log.PanicIf(err) + case TypeAsciiNoNul: + var err error + + value, err = parser.ParseAsciiNoNul(rawBytes, unitCount) + log.PanicIf(err) + case TypeShort: + var err error + + value, err = parser.ParseShorts(rawBytes, unitCount, byteOrder) + log.PanicIf(err) + case TypeLong: + var err error + + value, err = parser.ParseLongs(rawBytes, unitCount, byteOrder) + log.PanicIf(err) + case TypeFloat: + var err error + + value, err = parser.ParseFloats(rawBytes, unitCount, byteOrder) + log.PanicIf(err) + case TypeDouble: + var err error + + value, err = parser.ParseDoubles(rawBytes, unitCount, byteOrder) + log.PanicIf(err) + case TypeRational: + var err error + + value, err = parser.ParseRationals(rawBytes, unitCount, byteOrder) + log.PanicIf(err) + case TypeSignedLong: + var err error + + value, err = parser.ParseSignedLongs(rawBytes, unitCount, byteOrder) + log.PanicIf(err) + case TypeSignedRational: + var err error + + value, err = parser.ParseSignedRationals(rawBytes, unitCount, byteOrder) + log.PanicIf(err) + default: + // Affects only "unknown" values, in general. + log.Panicf("value of type [%s] can not be formatted into string", tagType.String()) + + // Never called. + return "", nil + } + + phrase, err = FormatFromType(value, justFirst) + log.PanicIf(err) + + return phrase, nil +} + +// TranslateStringToType converts user-provided strings to properly-typed +// values. If a string, returns a string. Else, assumes that it's a single +// number. If a list needs to be processed, it is the caller's responsibility to +// split it (according to whichever convention has been established). +func TranslateStringToType(tagType TagTypePrimitive, valueString string) (value interface{}, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tagType == TypeUndefined { + // The caller should just call String() on the decoded type. + log.Panicf("undefined-type values are not supported") + } + + if tagType == TypeByte { + wide, err := strconv.ParseInt(valueString, 16, 8) + log.PanicIf(err) + + return byte(wide), 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 == TypeFloat { + n, err := strconv.ParseFloat(valueString, 32) + log.PanicIf(err) + + return float32(n), nil + } else if tagType == TypeDouble { + n, err := strconv.ParseFloat(valueString, 64) + log.PanicIf(err) + + return float64(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 +} + +// GetTypeByName returns the `TagTypePrimitive` for the given type name. +// Returns (0) if not valid. +func GetTypeByName(typeName string) (tagType TagTypePrimitive, found bool) { + tagType, found = typeNamesR[typeName] + return tagType, found +} + +// BasicTag describes a single tag for any purpose. +type BasicTag struct { + // FqIfdPath is the fully-qualified IFD-path. + FqIfdPath string + + // IfdPath is the unindexed IFD-path. + IfdPath string + + // TagId is the tag-ID. + TagId uint16 +} + +func init() { + for typeId, typeName := range TypeNames { + typeNamesR[typeName] = typeId + } +} diff --git a/common/type_test.go b/common/type_test.go new file mode 100644 index 0000000..bc9008d --- /dev/null +++ b/common/type_test.go @@ -0,0 +1,437 @@ +package exifcommon + +import ( + "math" + "testing" + + "github.com/dsoprea/go-logging" +) + +func TestTypeByte_String(t *testing.T) { + if TypeByte.String() != "BYTE" { + t.Fatalf("Type name not correct (byte): [%s]", TypeByte.String()) + } +} + +func TestTypeAscii_String(t *testing.T) { + if TypeAscii.String() != "ASCII" { + t.Fatalf("Type name not correct (ASCII): [%s]", TypeAscii.String()) + } +} + +func TestTypeAsciiNoNul_String(t *testing.T) { + if TypeAsciiNoNul.String() != "_ASCII_NO_NUL" { + t.Fatalf("Type name not correct (ASCII no-NUL): [%s]", TypeAsciiNoNul.String()) + } +} + +func TestTypeShort_String(t *testing.T) { + if TypeShort.String() != "SHORT" { + t.Fatalf("Type name not correct (short): [%s]", TypeShort.String()) + } +} + +func TestTypeLong_String(t *testing.T) { + if TypeLong.String() != "LONG" { + t.Fatalf("Type name not correct (long): [%s]", TypeLong.String()) + } +} + +func TestTypeRational_String(t *testing.T) { + if TypeRational.String() != "RATIONAL" { + t.Fatalf("Type name not correct (rational): [%s]", TypeRational.String()) + } +} + +func TestTypeSignedLong_String(t *testing.T) { + if TypeSignedLong.String() != "SLONG" { + t.Fatalf("Type name not correct (signed long): [%s]", TypeSignedLong.String()) + } +} + +func TestTypeSignedRational_String(t *testing.T) { + if TypeSignedRational.String() != "SRATIONAL" { + t.Fatalf("Type name not correct (signed rational): [%s]", TypeSignedRational.String()) + } +} + +func TestTypeFloat_String(t *testing.T) { + if TypeFloat.String() != "FLOAT" { + t.Fatalf("Type name not correct (float): [%s]", TypeFloat.String()) + } +} + +func TestTypeDouble_String(t *testing.T) { + if TypeDouble.String() != "DOUBLE" { + t.Fatalf("Type name not correct (double): [%s]", TypeDouble.String()) + } +} + +func TestTypeByte_Size(t *testing.T) { + if TypeByte.Size() != 1 { + t.Fatalf("Type size not correct (byte): (%d)", TypeByte.Size()) + } +} + +func TestTypeAscii_Size(t *testing.T) { + if TypeAscii.Size() != 1 { + t.Fatalf("Type size not correct (ASCII): (%d)", TypeAscii.Size()) + } +} + +func TestTypeAsciiNoNul_Size(t *testing.T) { + if TypeAsciiNoNul.Size() != 1 { + t.Fatalf("Type size not correct (ASCII no-NUL): (%d)", TypeAsciiNoNul.Size()) + } +} + +func TestTypeShort_Size(t *testing.T) { + if TypeShort.Size() != 2 { + t.Fatalf("Type size not correct (short): (%d)", TypeShort.Size()) + } +} + +func TestTypeLong_Size(t *testing.T) { + if TypeLong.Size() != 4 { + t.Fatalf("Type size not correct (long): (%d)", TypeLong.Size()) + } +} + +func TestTypeRational_Size(t *testing.T) { + if TypeRational.Size() != 8 { + t.Fatalf("Type size not correct (rational): (%d)", TypeRational.Size()) + } +} + +func TestTypeSignedLong_Size(t *testing.T) { + if TypeSignedLong.Size() != 4 { + t.Fatalf("Type size not correct (signed long): (%d)", TypeSignedLong.Size()) + } +} + +func TestTypeSignedRational_Size(t *testing.T) { + if TypeSignedRational.Size() != 8 { + t.Fatalf("Type size not correct (signed rational): (%d)", TypeSignedRational.Size()) + } +} + +func TestTypeFloat_Size(t *testing.T) { + if TypeFloat.Size() != 4 { + t.Fatalf("Type size not correct (float): (%d)", TypeFloat.Size()) + } +} + +func TestTypeDouble_Size(t *testing.T) { + if TypeDouble.Size() != 8 { + t.Fatalf("Type size not correct (double): (%d)", TypeDouble.Size()) + } +} + +func TestFormat__Byte(t *testing.T) { + r := []byte{1, 2, 3, 4, 5, 6, 7, 8} + + s, err := FormatFromBytes(r, TypeByte, false, TestDefaultByteOrder) + log.PanicIf(err) + + if s != "01 02 03 04 05 06 07 08" { + t.Fatalf("Format output not correct (bytes): [%s]", s) + } +} + +func TestFormat__Ascii(t *testing.T) { + r := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 0} + + s, err := FormatFromBytes(r, TypeAscii, false, TestDefaultByteOrder) + log.PanicIf(err) + + if s != "abcdefg" { + t.Fatalf("Format output not correct (ASCII): [%s]", s) + } +} + +func TestFormat__AsciiNoNul(t *testing.T) { + r := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'} + + s, err := FormatFromBytes(r, TypeAsciiNoNul, false, TestDefaultByteOrder) + log.PanicIf(err) + + if s != "abcdefgh" { + t.Fatalf("Format output not correct (ASCII no-NUL): [%s]", s) + } +} + +func TestFormat__Short(t *testing.T) { + r := []byte{0, 1, 0, 2} + + s, err := FormatFromBytes(r, TypeShort, false, TestDefaultByteOrder) + log.PanicIf(err) + + if s != "[1 2]" { + t.Fatalf("Format output not correct (shorts): [%s]", s) + } +} + +func TestFormat__Long(t *testing.T) { + r := []byte{0, 0, 0, 1, 0, 0, 0, 2} + + s, err := FormatFromBytes(r, TypeLong, false, TestDefaultByteOrder) + log.PanicIf(err) + + if s != "[1 2]" { + t.Fatalf("Format output not correct (longs): [%s]", s) + } +} + +func TestFormat__Float(t *testing.T) { + r := []byte{0x3f, 0x80, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00} + + s, err := FormatFromBytes(r, TypeFloat, false, TestDefaultByteOrder) + log.PanicIf(err) + + if s != "[1 2]" { + t.Fatalf("Format output not correct (floats): [%s]", s) + } +} + +func TestFormat__Double(t *testing.T) { + r := []byte{0x40, 0x09, 0x21, 0xfb, 0x53, 0xc8, 0xd4, 0xf1, + 0x40, 0x05, 0xbf, 0x0a, 0x89, 0xf1, 0xb0, 0xdd} + + s, err := FormatFromBytes(r, TypeDouble, false, TestDefaultByteOrder) + log.PanicIf(err) + + if s != "[3.14159265 2.71828182]" { + t.Fatalf("Format output not correct (doubles): [%s]", s) + } +} + +func TestFormat__Rational(t *testing.T) { + r := []byte{ + 0, 0, 0, 1, 0, 0, 0, 2, + 0, 0, 0, 3, 0, 0, 0, 4, + } + + s, err := FormatFromBytes(r, TypeRational, false, TestDefaultByteOrder) + log.PanicIf(err) + + if s != "[1/2 3/4]" { + t.Fatalf("Format output not correct (rationals): [%s]", s) + } +} + +func TestFormat__SignedLong(t *testing.T) { + r := []byte{0, 0, 0, 1, 0, 0, 0, 2} + + s, err := FormatFromBytes(r, TypeSignedLong, false, TestDefaultByteOrder) + log.PanicIf(err) + + if s != "[1 2]" { + t.Fatalf("Format output not correct (signed longs): [%s]", s) + } +} + +func TestFormat__SignedRational(t *testing.T) { + r := []byte{ + 0, 0, 0, 1, 0, 0, 0, 2, + 0, 0, 0, 3, 0, 0, 0, 4, + } + + s, err := FormatFromBytes(r, TypeSignedRational, false, TestDefaultByteOrder) + log.PanicIf(err) + + if s != "[1/2 3/4]" { + t.Fatalf("Format output not correct (signed rationals): [%s]", s) + } +} + +func TestFormat__Undefined(t *testing.T) { + r := []byte{'a', 'b'} + + _, err := FormatFromBytes(r, TypeUndefined, false, TestDefaultByteOrder) + if err == nil { + t.Fatalf("Expected error.") + } else if err.Error() != "can not determine tag-value size for type (7): [UNDEFINED]" { + log.Panic(err) + } +} + +func TestTranslateStringToType__TypeUndefined(t *testing.T) { + _, err := TranslateStringToType(TypeUndefined, "") + if err == nil { + t.Fatalf("Expected error.") + } else if err.Error() != "undefined-type values are not supported" { + log.Panic(err) + } +} + +func TestTranslateStringToType__TypeByte(t *testing.T) { + v, err := TranslateStringToType(TypeByte, "02") + log.PanicIf(err) + + if v != byte(2) { + t.Fatalf("Translation of string to type not correct (bytes): %v", v) + } +} + +func TestTranslateStringToType__TypeAscii(t *testing.T) { + v, err := TranslateStringToType(TypeAscii, "abcdefgh") + log.PanicIf(err) + + if v != "abcdefgh" { + t.Fatalf("Translation of string to type not correct (ascii): %v", v) + } +} + +func TestTranslateStringToType__TypeAsciiNoNul(t *testing.T) { + v, err := TranslateStringToType(TypeAsciiNoNul, "abcdefgh") + log.PanicIf(err) + + if v != "abcdefgh" { + t.Fatalf("Translation of string to type not correct (ascii no-NUL): %v", v) + } +} + +func TestTranslateStringToType__TypeShort(t *testing.T) { + v, err := TranslateStringToType(TypeShort, "11") + log.PanicIf(err) + + if v != uint16(11) { + t.Fatalf("Translation of string to type not correct (short): %v", v) + } +} + +func TestTranslateStringToType__TypeLong(t *testing.T) { + v, err := TranslateStringToType(TypeLong, "11") + log.PanicIf(err) + + if v != uint32(11) { + t.Fatalf("Translation of string to type not correct (long): %v", v) + } +} + +func TestTranslateStringToType__TypeFloat(t *testing.T) { + v, err := TranslateStringToType(TypeFloat, "3.14159265") + log.PanicIf(err) + + expected := float32(3.14159265) + if v.(float32) < expected || v.(float32) >= math.Nextafter32(expected, expected+1) { + t.Fatalf("Translation of string to type not correct (float32): %v", v) + } +} + +func TestTranslateStringToType__TypeDouble(t *testing.T) { + v, err := TranslateStringToType(TypeDouble, "3.14159265") + log.PanicIf(err) + + expected := float64(3.14159265) + if v.(float64) < expected || v.(float64) >= math.Nextafter(expected, expected+1) { + t.Fatalf("Translation of string to type not correct (double): %v", v) + } +} + +func TestTranslateStringToType__TypeRational(t *testing.T) { + v, err := TranslateStringToType(TypeRational, "11/22") + log.PanicIf(err) + + r := v.(Rational) + + if r.Numerator != 11 || r.Denominator != 22 { + t.Fatalf("Translation of string to type not correct (rational): %v", r) + } +} + +func TestTranslateStringToType__TypeSignedLong(t *testing.T) { + v, err := TranslateStringToType(TypeSignedLong, "11") + log.PanicIf(err) + + if v != int32(11) { + t.Fatalf("Translation of string to type not correct (signed long): %v", v) + } +} + +func TestTranslateStringToType__TypeSignedRational(t *testing.T) { + v, err := TranslateStringToType(TypeSignedRational, "11/22") + log.PanicIf(err) + + r := v.(SignedRational) + + if r.Numerator != 11 || r.Denominator != 22 { + t.Fatalf("Translation of string to type not correct (signed rational): %v", r) + } +} + +func TestTranslateStringToType__InvalidType(t *testing.T) { + _, err := TranslateStringToType(99, "11/22") + if err == nil { + t.Fatalf("Expected error for invalid type.") + } else if err.Error() != "from-string encoding for type not supported; this shouldn't happen: []" { + log.Panic(err) + } +} + +// } 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 TestIsPrintableText_letters(t *testing.T) { + if isPrintableText("abc") != true { + t.Fatalf("Printable text interpreted as nonprintable.") + } +} + +func TestIsPrintableText_space(t *testing.T) { + if isPrintableText(" ") != true { + t.Fatalf("Printable text interpreted as nonprintable.") + } +} + +func TestIsPrintableText_newlines(t *testing.T) { + if isPrintableText("\r\n") != true { + t.Fatalf("Printable text interpreted as nonprintable.") + } +} + +func TestIsPrintableText_punctuationAndSymbols(t *testing.T) { + if isPrintableText(",:-/$©") != true { + t.Fatalf("Printable text interpreted as nonprintable.") + } +} diff --git a/common/utility.go b/common/utility.go new file mode 100644 index 0000000..5750497 --- /dev/null +++ b/common/utility.go @@ -0,0 +1,148 @@ +package exifcommon + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" + "time" + + "github.com/dsoprea/go-logging" +) + +var ( + timeType = reflect.TypeOf(time.Time{}) +) + +// DumpBytes prints a list of hex-encoded bytes. +func DumpBytes(data []byte) { + fmt.Printf("DUMP: ") + for _, x := range data { + fmt.Printf("%02x ", x) + } + + fmt.Printf("\n") +} + +// DumpBytesClause prints a list like DumpBytes(), but encapsulated in +// "[]byte { ... }". +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") +} + +// DumpBytesToString returns a stringified list of hex-encoded bytes. +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() +} + +// DumpBytesClauseToString returns a comma-separated list of hex-encoded bytes. +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() +} + +// 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()) +} + +// 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] + + // Normalize the separators. + datestampValue = strings.ReplaceAll(datestampValue, "-", ":") + timestampValue = strings.ReplaceAll(timestampValue, "-", ":") + + 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 +} + +// IsTime returns true if the value is a `time.Time`. +func IsTime(v interface{}) bool { + + // TODO(dustin): Add test + + return reflect.TypeOf(v) == timeType +} diff --git a/common/utility_test.go b/common/utility_test.go new file mode 100644 index 0000000..42b0c35 --- /dev/null +++ b/common/utility_test.go @@ -0,0 +1,80 @@ +package exifcommon + +import ( + "fmt" + "testing" + "time" + + "github.com/dsoprea/go-logging" +) + +func TestDumpBytes(t *testing.T) { + DumpBytes([]byte{1, 2, 3, 4}) +} + +func TestDumpBytesClause(t *testing.T) { + DumpBytesClause([]byte{1, 2, 3, 4}) +} + +func TestDumpBytesToString(t *testing.T) { + s := DumpBytesToString([]byte{1, 2, 3, 4}) + if s != "01 02 03 04" { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestDumpBytesClauseToString(t *testing.T) { + s := DumpBytesClauseToString([]byte{1, 2, 3, 4}) + if s != "0x01, 0x02, 0x03, 0x04" { + t.Fatalf("Stringified clause is not correct: [%s]", s) + } +} + +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 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] +} + +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 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] +} diff --git a/common/value_context.go b/common/value_context.go new file mode 100644 index 0000000..b9e6341 --- /dev/null +++ b/common/value_context.go @@ -0,0 +1,464 @@ +package exifcommon + +import ( + "errors" + "io" + + "encoding/binary" + + "github.com/dsoprea/go-logging" +) + +var ( + parser *Parser +) + +var ( + // ErrNotFarValue indicates that an offset-based lookup was attempted for a + // non-offset-based (embedded) value. + ErrNotFarValue = errors.New("not a far value") +) + +// ValueContext embeds all of the parameters required to find and extract the +// actual tag value. +type ValueContext struct { + unitCount uint32 + valueOffset uint32 + rawValueOffset []byte + rs io.ReadSeeker + + tagType TagTypePrimitive + byteOrder binary.ByteOrder + + // undefinedValueTagType is the effective type to use if this is an + // "undefined" value. + undefinedValueTagType TagTypePrimitive + + ifdPath string + tagId uint16 +} + +// TODO(dustin): We can update newValueContext() to derive `valueOffset` itself (from `rawValueOffset`). + +// NewValueContext returns a new ValueContext struct. +func NewValueContext(ifdPath string, tagId uint16, unitCount, valueOffset uint32, rawValueOffset []byte, rs io.ReadSeeker, tagType TagTypePrimitive, byteOrder binary.ByteOrder) *ValueContext { + return &ValueContext{ + unitCount: unitCount, + valueOffset: valueOffset, + rawValueOffset: rawValueOffset, + rs: rs, + + tagType: tagType, + byteOrder: byteOrder, + + ifdPath: ifdPath, + tagId: tagId, + } +} + +// SetUndefinedValueType sets the effective type if this is an unknown-type tag. +func (vc *ValueContext) SetUndefinedValueType(tagType TagTypePrimitive) { + if vc.tagType != TypeUndefined { + log.Panicf("can not set effective type for unknown-type tag because this is *not* an unknown-type tag") + } + + vc.undefinedValueTagType = tagType +} + +// UnitCount returns the embedded unit-count. +func (vc *ValueContext) UnitCount() uint32 { + return vc.unitCount +} + +// ValueOffset returns the value-offset decoded as a `uint32`. +func (vc *ValueContext) ValueOffset() uint32 { + return vc.valueOffset +} + +// RawValueOffset returns the uninterpreted value-offset. This is used for +// embedded values (values small enough to fit within the offset bytes rather +// than needing to be stored elsewhere and referred to by an actual offset). +func (vc *ValueContext) RawValueOffset() []byte { + return vc.rawValueOffset +} + +// AddressableData returns the block of data that we can dereference into. +func (vc *ValueContext) AddressableData() io.ReadSeeker { + + // RELEASE)dustin): Rename from AddressableData() to ReadSeeker() + + return vc.rs +} + +// ByteOrder returns the byte-order of numbers. +func (vc *ValueContext) ByteOrder() binary.ByteOrder { + return vc.byteOrder +} + +// IfdPath returns the path of the IFD containing this tag. +func (vc *ValueContext) IfdPath() string { + return vc.ifdPath +} + +// TagId returns the ID of the tag that we represent. +func (vc *ValueContext) TagId() uint16 { + return vc.tagId +} + +// 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 +} + +// SizeInBytes returns the number of bytes that this value requires. The +// underlying call will panic if the type is UNDEFINED. It is the +// responsibility of the caller to preemptively check that. +func (vc *ValueContext) SizeInBytes() int { + tagType := vc.effectiveValueType() + + return tagType.Size() * int(vc.unitCount) +} + +// effectiveValueType returns the effective type of the unknown-type tag or, if +// not unknown, the actual type. +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 +} + +// readRawEncoded returns the encoded bytes for the value that we represent. +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 + } + + _, err = vc.rs.Seek(int64(vc.valueOffset), io.SeekStart) + log.PanicIf(err) + + rawBytes = make([]byte, vc.unitCount*unitSizeRaw) + + _, err = io.ReadFull(vc.rs, rawBytes) + log.PanicIf(err) + + return rawBytes, nil +} + +// GetFarOffset returns the offset if the value is not embedded [within the +// pointer itself] or an error if an embedded value. +func (vc *ValueContext) GetFarOffset() (offset uint32, err error) { + if vc.isEmbedded() == true { + return 0, ErrNotFarValue + } + + return vc.valueOffset, nil +} + +// ReadRawEncoded returns the encoded bytes for the value that we represent. +func (vc *ValueContext) ReadRawEncoded() (rawBytes []byte, err error) { + + // TODO(dustin): Remove this method and rename readRawEncoded in its place. + + return vc.readRawEncoded() +} + +// 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 := FormatFromBytes(rawBytes, vc.effectiveValueType(), false, vc.byteOrder) + log.PanicIf(err) + + return phrase, nil +} + +// FormatFirst 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 := FormatFromBytes(rawBytes, vc.tagType, true, vc.byteOrder) + log.PanicIf(err) + + return phrase, nil +} + +// ReadBytes parses the encoded byte-array from the value-context. +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 +} + +// ReadAscii parses the encoded NUL-terminated ASCII string from the value- +// context. +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 +} + +// ReadAsciiNoNul parses the non-NUL-terminated encoded ASCII string from the +// value-context. +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 +} + +// ReadShorts parses the list of encoded shorts from the value-context. +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 +} + +// ReadLongs parses the list of encoded, unsigned longs from the value-context. +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 +} + +// ReadFloats parses the list of encoded, floats from the value-context. +func (vc *ValueContext) ReadFloats() (value []float32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rawValue, err := vc.readRawEncoded() + log.PanicIf(err) + + value, err = parser.ParseFloats(rawValue, vc.unitCount, vc.byteOrder) + log.PanicIf(err) + + return value, nil +} + +// ReadDoubles parses the list of encoded, doubles from the value-context. +func (vc *ValueContext) ReadDoubles() (value []float64, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rawValue, err := vc.readRawEncoded() + log.PanicIf(err) + + value, err = parser.ParseDoubles(rawValue, vc.unitCount, vc.byteOrder) + log.PanicIf(err) + + return value, nil +} + +// ReadRationals parses the list of encoded, unsigned rationals from the value- +// context. +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 +} + +// ReadSignedLongs parses the list of encoded, signed longs from the value-context. +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 +} + +// ReadSignedRationals parses the list of encoded, signed rationals from the +// value-context. +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 == TypeFloat { + values, err = vc.ReadFloats() + log.PanicIf(err) + } else if vc.tagType == TypeDouble { + values, err = vc.ReadDoubles() + 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 +} + +func init() { + parser = new(Parser) +} diff --git a/common/value_context_test.go b/common/value_context_test.go new file mode 100644 index 0000000..8b4e93f --- /dev/null +++ b/common/value_context_test.go @@ -0,0 +1,1308 @@ +package exifcommon + +import ( + "bytes" + "math" + "reflect" + "testing" + + "io/ioutil" + + "github.com/dsoprea/go-logging" + "github.com/dsoprea/go-utility/v2/filesystem" +) + +func TestNewValueContext(t *testing.T) { + rawValueOffset := []byte{0, 0, 0, 22} + addressableData := []byte{1, 2, 3, 4} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + 11, + 22, + rawValueOffset, + sb, + TypeLong, + TestDefaultByteOrder) + + if vc.ifdPath != "aa/bb" { + t.Fatalf("ifdPath not correct: [%s]", vc.ifdPath) + } else if vc.tagId != 0x1234 { + t.Fatalf("tagId not correct: (0x%04x)", vc.tagId) + } else if vc.unitCount != 11 { + t.Fatalf("unitCount not correct: (%d)", vc.unitCount) + } else if vc.valueOffset != 22 { + t.Fatalf("valueOffset not correct: (%d)", vc.valueOffset) + } else if bytes.Equal(vc.rawValueOffset, rawValueOffset) != true { + t.Fatalf("rawValueOffset not correct: %v", vc.rawValueOffset) + } else if vc.tagType != TypeLong { + t.Fatalf("tagType not correct: (%d)", vc.tagType) + } else if vc.byteOrder != TestDefaultByteOrder { + t.Fatalf("byteOrder not correct: %v", vc.byteOrder) + } + + recoveredBytes, err := ioutil.ReadAll(vc.AddressableData()) + log.PanicIf(err) + + if bytes.Equal(recoveredBytes, addressableData) != true { + t.Fatalf("AddressableData() not correct: %v", recoveredBytes) + } +} + +func TestValueContext_SetUndefinedValueType__ErrorWhenNotUndefined(t *testing.T) { + defer func() { + if errRaw := recover(); errRaw != nil { + err := errRaw.(error) + if err.Error() != "can not set effective type for unknown-type tag because this is *not* an unknown-type tag" { + t.Fatalf("Error not expected: [%s]", err.Error()) + } + + return + } + + t.Fatalf("Expected error.") + }() + + rawValueOffset := []byte{0, 0, 0, 22} + + addressableData := []byte{1, 2, 3, 4} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + 11, + 22, + rawValueOffset, + sb, + TypeLong, + TestDefaultByteOrder) + + vc.SetUndefinedValueType(TypeLong) +} + +func TestValueContext_SetUndefinedValueType__Ok(t *testing.T) { + rawValueOffset := []byte{0, 0, 0, 22} + + addressableData := []byte{1, 2, 3, 4} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + 11, + 22, + rawValueOffset, + sb, + TypeUndefined, + TestDefaultByteOrder) + + vc.SetUndefinedValueType(TypeLong) + + if vc.tagType != TypeUndefined { + t.Fatalf("Internal type not still 'undefined': (%d)", vc.tagType) + } else if vc.undefinedValueTagType != TypeLong { + t.Fatalf("Internal undefined-type not correct: (%d)", vc.undefinedValueTagType) + } else if vc.effectiveValueType() != TypeLong { + t.Fatalf("Effective tag not correct: (%d)", vc.effectiveValueType()) + } +} + +func TestValueContext_effectiveValueType(t *testing.T) { + rawValueOffset := []byte{0, 0, 0, 22} + + addressableData := []byte{1, 2, 3, 4} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + 11, + 22, + rawValueOffset, + sb, + TypeUndefined, + TestDefaultByteOrder) + + vc.SetUndefinedValueType(TypeLong) + + if vc.tagType != TypeUndefined { + t.Fatalf("Internal type not still 'undefined': (%d)", vc.tagType) + } else if vc.undefinedValueTagType != TypeLong { + t.Fatalf("Internal undefined-type not correct: (%d)", vc.undefinedValueTagType) + } else if vc.effectiveValueType() != TypeLong { + t.Fatalf("Effective tag not correct: (%d)", vc.effectiveValueType()) + } +} + +func TestValueContext_UnitCount(t *testing.T) { + rawValueOffset := []byte{0, 0, 0, 22} + + addressableData := []byte{1, 2, 3, 4} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + 11, + 22, + rawValueOffset, + sb, + TypeUndefined, + TestDefaultByteOrder) + + if vc.UnitCount() != 11 { + t.Fatalf("UnitCount() not correct: (%d)", vc.UnitCount()) + } +} + +func TestValueContext_ValueOffset(t *testing.T) { + rawValueOffset := []byte{0, 0, 0, 22} + + addressableData := []byte{1, 2, 3, 4} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + 11, + 22, + rawValueOffset, + sb, + TypeUndefined, + TestDefaultByteOrder) + + if vc.ValueOffset() != 22 { + t.Fatalf("ValueOffset() not correct: (%d)", vc.ValueOffset()) + } +} + +func TestValueContext_RawValueOffset(t *testing.T) { + rawValueOffset := []byte{0, 0, 0, 22} + + addressableData := []byte{1, 2, 3, 4} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + 11, + 22, + rawValueOffset, + sb, + TypeUndefined, + TestDefaultByteOrder) + + if bytes.Equal(vc.RawValueOffset(), rawValueOffset) != true { + t.Fatalf("RawValueOffset() not correct: %v", vc.RawValueOffset()) + } +} + +func TestValueContext_AddressableData(t *testing.T) { + rawValueOffset := []byte{0, 0, 0, 22} + + addressableData := []byte{1, 2, 3, 4} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + 11, + 22, + rawValueOffset, + sb, + TypeUndefined, + TestDefaultByteOrder) + + recoveredBytes, err := ioutil.ReadAll(vc.AddressableData()) + log.PanicIf(err) + + if bytes.Equal(recoveredBytes, addressableData) != true { + t.Fatalf("AddressableData() not correct: %v", recoveredBytes) + } +} + +func TestValueContext_ByteOrder(t *testing.T) { + rawValueOffset := []byte{0, 0, 0, 22} + + addressableData := []byte{1, 2, 3, 4} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + 11, + 22, + rawValueOffset, + sb, + TypeUndefined, + TestDefaultByteOrder) + + if vc.ByteOrder() != TestDefaultByteOrder { + t.Fatalf("ByteOrder() not correct: %v", vc.ByteOrder()) + } +} + +func TestValueContext_IfdPath(t *testing.T) { + rawValueOffset := []byte{0, 0, 0, 22} + + addressableData := []byte{1, 2, 3, 4} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + 11, + 22, + rawValueOffset, + sb, + TypeUndefined, + TestDefaultByteOrder) + + if vc.IfdPath() != "aa/bb" { + t.Fatalf("IfdPath() not correct: [%s]", vc.IfdPath()) + } +} + +func TestValueContext_TagId(t *testing.T) { + rawValueOffset := []byte{0, 0, 0, 22} + + addressableData := []byte{1, 2, 3, 4} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + 11, + 22, + rawValueOffset, + sb, + TypeUndefined, + TestDefaultByteOrder) + + if vc.TagId() != 0x1234 { + t.Fatalf("TagId() not correct: (%d)", vc.TagId()) + } +} + +func TestValueContext_isEmbedded__True(t *testing.T) { + unitCount := uint32(4) + rawValueOffset := []byte{0, 0, 0, 22} + + addressableData := []byte{1, 2, 3, 4} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + 22, + rawValueOffset, + sb, + TypeByte, + TestDefaultByteOrder) + + if vc.isEmbedded() != true { + t.Fatalf("isEmbedded() not correct: %v", vc.isEmbedded()) + } +} + +func TestValueContext_isEmbedded__False(t *testing.T) { + unitCount := uint32(5) + rawValueOffset := []byte{0, 0, 0, 22} + + addressableData := []byte{1, 2, 3, 4} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + 22, + rawValueOffset, + sb, + TypeByte, + TestDefaultByteOrder) + + if vc.isEmbedded() != false { + t.Fatalf("isEmbedded() not correct: %v", vc.isEmbedded()) + } +} + +func TestValueContext_readRawEncoded__IsEmbedded(t *testing.T) { + unitCount := uint32(4) + + rawValueOffset := []byte{1, 2, 3, 4} + + // Ignored, in this case. + valueOffset := uint32(0) + + addressableData := []byte{} + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeByte, + TestDefaultByteOrder) + + recovered, err := vc.readRawEncoded() + log.PanicIf(err) + + if bytes.Equal(recovered, rawValueOffset) != true { + t.Fatalf("Embedded value bytes not recovered correctly: %v", recovered) + } +} + +func TestValueContext_readRawEncoded__IsRelative(t *testing.T) { + unitCount := uint32(5) + + // Ignored, in this case. + rawValueOffset := []byte{0, 0, 0, 0} + + valueOffset := uint32(4) + + data := []byte{5, 6, 7, 8, 9} + + addressableData := []byte{1, 2, 3, 4} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeByte, + TestDefaultByteOrder) + + recovered, err := vc.readRawEncoded() + log.PanicIf(err) + + if bytes.Equal(recovered, data) != true { + t.Fatalf("Relative value bytes not recovered correctly: %v", recovered) + } +} + +func TestValueContext_Format__Byte(t *testing.T) { + unitCount := uint32(8) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeByte, + TestDefaultByteOrder) + + value, err := vc.Format() + log.PanicIf(err) + + if value != "61 62 63 64 65 66 67 68" { + t.Fatalf("Format not correct for bytes: [%s]", value) + } +} + +func TestValueContext_Format__Ascii(t *testing.T) { + unitCount := uint32(8) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 0} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeAscii, + TestDefaultByteOrder) + + value, err := vc.Format() + log.PanicIf(err) + + if value != "abcdefg" { + t.Fatalf("Format not correct for ASCII: [%s]", value) + } +} + +func TestValueContext_Format__AsciiNoNul(t *testing.T) { + unitCount := uint32(8) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeAsciiNoNul, + TestDefaultByteOrder) + + value, err := vc.Format() + log.PanicIf(err) + + if value != "abcdefgh" { + t.Fatalf("Format not correct for ASCII (no NUL): [%s]", value) + } +} + +func TestValueContext_Format__Short(t *testing.T) { + unitCount := uint32(4) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{0, 1, 0, 2, 0, 3, 0, 4} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeShort, + TestDefaultByteOrder) + + value, err := vc.Format() + log.PanicIf(err) + + if value != "[1 2 3 4]" { + t.Fatalf("Format not correct for shorts: [%s]", value) + } +} + +func TestValueContext_Format__Long(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{0, 0, 0, 1, 0, 0, 0, 2} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeLong, + TestDefaultByteOrder) + + value, err := vc.Format() + log.PanicIf(err) + + if value != "[1 2]" { + t.Fatalf("Format not correct for longs: [%s]", value) + } +} + +func TestValueContext_Format__Rational(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{ + 0, 0, 0, 1, 0, 0, 0, 2, + 0, 0, 0, 3, 0, 0, 0, 4, + } + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeRational, + TestDefaultByteOrder) + + value, err := vc.Format() + log.PanicIf(err) + + if value != "[1/2 3/4]" { + t.Fatalf("Format not correct for rationals: [%s]", value) + } +} + +func TestValueContext_Format__SignedLong(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{0, 0, 0, 1, 0, 0, 0, 2} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeSignedLong, + TestDefaultByteOrder) + + value, err := vc.Format() + log.PanicIf(err) + + if value != "[1 2]" { + t.Fatalf("Format not correct for signed-longs: [%s]", value) + } +} + +func TestValueContext_Format__SignedRational(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{ + 0, 0, 0, 1, 0, 0, 0, 2, + 0, 0, 0, 3, 0, 0, 0, 4, + } + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeSignedRational, + TestDefaultByteOrder) + + value, err := vc.Format() + log.PanicIf(err) + + if value != "[1/2 3/4]" { + t.Fatalf("Format not correct for signed-rationals: [%s]", value) + } +} + +func TestValueContext_Format__Undefined__NoEffectiveType(t *testing.T) { + defer func() { + if errRaw := recover(); errRaw != nil { + err := errRaw.(error) + if err.Error() != "undefined-value type not set" { + t.Fatalf("Error not expected: [%s]", err.Error()) + } + + return + } + + t.Fatalf("Expected error.") + }() + + unitCount := uint32(8) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeUndefined, + TestDefaultByteOrder) + + value, err := vc.Format() + log.PanicIf(err) + + if value != "61 62 63 64 65 66 67 68" { + t.Fatalf("Format not correct for bytes: [%s]", value) + } +} + +func TestValueContext_Format__Undefined__HasEffectiveType(t *testing.T) { + unitCount := uint32(8) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 0} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeUndefined, + TestDefaultByteOrder) + + vc.SetUndefinedValueType(TypeAscii) + + value, err := vc.Format() + log.PanicIf(err) + + if value != "abcdefg" { + t.Fatalf("Format not correct for undefined (with effective type of string): [%s]", value) + } +} + +func TestValueContext_FormatFirst__Bytes(t *testing.T) { + unitCount := uint32(8) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeByte, + TestDefaultByteOrder) + + value, err := vc.FormatFirst() + log.PanicIf(err) + + if value != "61 62 63 64 65 66 67 68" { + t.Fatalf("FormatFirst not correct for bytes: [%s]", value) + } +} + +func TestValueContext_FormatFirst__String(t *testing.T) { + unitCount := uint32(8) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 0} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeAscii, + TestDefaultByteOrder) + + value, err := vc.FormatFirst() + log.PanicIf(err) + + if value != "abcdefg" { + t.Fatalf("FormatFirst not correct for ASCII: [%s]", value) + } +} + +func TestValueContext_FormatFirst__List(t *testing.T) { + unitCount := uint32(4) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{0, 1, 0, 2, 0, 3, 0, 4} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeShort, + TestDefaultByteOrder) + + value, err := vc.FormatFirst() + log.PanicIf(err) + + if value != "1..." { + t.Fatalf("FormatFirst not correct for shorts: [%s]", value) + } +} + +func TestValueContext_ReadBytes(t *testing.T) { + unitCount := uint32(8) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'} + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeByte, + TestDefaultByteOrder) + + value, err := vc.ReadBytes() + log.PanicIf(err) + + if bytes.Equal(value, data) != true { + t.Fatalf("ReadBytes not correct: %v", value) + } +} + +func TestValueContext_ReadAscii(t *testing.T) { + unitCount := uint32(8) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 0} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeAscii, + TestDefaultByteOrder) + + value, err := vc.ReadAscii() + log.PanicIf(err) + + if value != "abcdefg" { + t.Fatalf("ReadAscii not correct: [%s]", value) + } +} + +func TestValueContext_ReadAsciiNoNul(t *testing.T) { + unitCount := uint32(8) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'} + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeAsciiNoNul, + TestDefaultByteOrder) + + value, err := vc.ReadAsciiNoNul() + log.PanicIf(err) + + if value != "abcdefgh" { + t.Fatalf("ReadAsciiNoNul not correct: [%s]", value) + } +} + +func TestValueContext_ReadShorts(t *testing.T) { + unitCount := uint32(4) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{0, 1, 0, 2, 0, 3, 0, 4} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeShort, + TestDefaultByteOrder) + + value, err := vc.ReadShorts() + log.PanicIf(err) + + if reflect.DeepEqual(value, []uint16{1, 2, 3, 4}) != true { + t.Fatalf("ReadShorts not correct: %v", value) + } +} + +func TestValueContext_ReadLongs(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{0, 0, 0, 1, 0, 0, 0, 2} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeLong, + TestDefaultByteOrder) + + value, err := vc.ReadLongs() + log.PanicIf(err) + + if reflect.DeepEqual(value, []uint32{1, 2}) != true { + t.Fatalf("ReadLongs not correct: %v", value) + } +} + +func TestValueContext_ReadFloats(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{0x40, 0x49, 0x0f, 0xdb, 0x40, 0x2d, 0xf8, 0x54} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeFloat, + TestDefaultByteOrder) + + value, err := vc.ReadFloats() + log.PanicIf(err) + + expectedResult := []float32{3.14159265, 2.71828182} + for i, v := range value { + if v < expectedResult[i] || v >= math.Nextafter32(expectedResult[i], expectedResult[i]+1) { + t.Fatalf("ReadFloats expecting %v, received %v", expectedResult[i], v) + } + } +} +func TestValueContext_ReadDoubles(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{0x40, 0x09, 0x21, 0xfb, 0x53, 0xc8, 0xd4, 0xf1, + 0x40, 0x05, 0xbf, 0x0a, 0x89, 0xf1, 0xb0, 0xdd} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeDouble, + TestDefaultByteOrder) + + value, err := vc.ReadDoubles() + log.PanicIf(err) + + expectedResult := []float64{3.14159265, 2.71828182} + for i, v := range value { + if v < expectedResult[i] || v >= math.Nextafter(expectedResult[i], expectedResult[i]+1) { + t.Fatalf("ReadDoubles expecting %v, received %v", expectedResult[i], v) + } + } +} + +func TestValueContext_ReadRationals(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{ + 0, 0, 0, 1, 0, 0, 0, 2, + 0, 0, 0, 3, 0, 0, 0, 4, + } + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeRational, + TestDefaultByteOrder) + + value, err := vc.ReadRationals() + log.PanicIf(err) + + expected := []Rational{ + {Numerator: 1, Denominator: 2}, + {Numerator: 3, Denominator: 4}, + } + + if reflect.DeepEqual(value, expected) != true { + t.Fatalf("ReadRationals not correct: %v", value) + } +} + +func TestValueContext_ReadSignedLongs(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{0, 0, 0, 1, 0, 0, 0, 2} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext( + "aa/bb", + 0x1234, + unitCount, + valueOffset, + rawValueOffset, + sb, + TypeSignedLong, + TestDefaultByteOrder) + + value, err := vc.ReadSignedLongs() + log.PanicIf(err) + + if reflect.DeepEqual(value, []int32{1, 2}) != true { + t.Fatalf("ReadSignedLongs not correct: %v", value) + } +} + +func TestValueContext_ReadSignedRationals(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{ + 0, 0, 0, 1, 0, 0, 0, 2, + 0, 0, 0, 3, 0, 0, 0, 4, + } + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, sb, TypeSignedRational, TestDefaultByteOrder) + + value, err := vc.ReadSignedRationals() + log.PanicIf(err) + + expected := []SignedRational{ + {Numerator: 1, Denominator: 2}, + {Numerator: 3, Denominator: 4}, + } + + if reflect.DeepEqual(value, expected) != true { + t.Fatalf("ReadSignedRationals not correct: %v", value) + } +} + +func TestValueContext_Values__Byte(t *testing.T) { + unitCount := uint32(8) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, sb, TypeByte, TestDefaultByteOrder) + + value, err := vc.Values() + log.PanicIf(err) + + if reflect.DeepEqual(value, data) != true { + t.Fatalf("Values not correct (bytes): %v", value) + } +} + +func TestValueContext_Values__Ascii(t *testing.T) { + unitCount := uint32(8) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 0} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, sb, TypeAscii, TestDefaultByteOrder) + + value, err := vc.Values() + log.PanicIf(err) + + if reflect.DeepEqual(value, "abcdefg") != true { + t.Fatalf("Values not correct (ASCII): [%s]", value) + } +} + +func TestValueContext_Values__AsciiNoNul(t *testing.T) { + unitCount := uint32(8) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, sb, TypeAsciiNoNul, TestDefaultByteOrder) + + value, err := vc.Values() + log.PanicIf(err) + + if reflect.DeepEqual(value, "abcdefgh") != true { + t.Fatalf("Values not correct (ASCII no-NUL): [%s]", value) + } +} + +func TestValueContext_Values__Short(t *testing.T) { + unitCount := uint32(4) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{0, 1, 0, 2, 0, 3, 0, 4} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, sb, TypeShort, TestDefaultByteOrder) + + value, err := vc.Values() + log.PanicIf(err) + + if reflect.DeepEqual(value, []uint16{1, 2, 3, 4}) != true { + t.Fatalf("Values not correct (shorts): %v", value) + } +} + +func TestValueContext_Values__Long(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{0, 0, 0, 1, 0, 0, 0, 2} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, sb, TypeLong, TestDefaultByteOrder) + + value, err := vc.Values() + log.PanicIf(err) + + if reflect.DeepEqual(value, []uint32{1, 2}) != true { + t.Fatalf("Values not correct (longs): %v", value) + } +} + +func TestValueContext_Values__Rational(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{ + 0, 0, 0, 1, 0, 0, 0, 2, + 0, 0, 0, 3, 0, 0, 0, 4, + } + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, sb, TypeRational, TestDefaultByteOrder) + + value, err := vc.Values() + log.PanicIf(err) + + expected := []Rational{ + {Numerator: 1, Denominator: 2}, + {Numerator: 3, Denominator: 4}, + } + + if reflect.DeepEqual(value, expected) != true { + t.Fatalf("Values not correct (rationals): %v", value) + } +} + +func TestValueContext_Values__SignedLong(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{0, 0, 0, 1, 0, 0, 0, 2} + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, sb, TypeSignedLong, TestDefaultByteOrder) + + value, err := vc.Values() + log.PanicIf(err) + + if reflect.DeepEqual(value, []int32{1, 2}) != true { + t.Fatalf("Values not correct (signed longs): %v", value) + } +} + +func TestValueContext_Values__SignedRational(t *testing.T) { + unitCount := uint32(2) + + rawValueOffset := []byte{0, 0, 0, 4} + valueOffset := uint32(4) + + data := []byte{ + 0, 0, 0, 1, 0, 0, 0, 2, + 0, 0, 0, 3, 0, 0, 0, 4, + } + + addressableData := []byte{0, 0, 0, 0} + addressableData = append(addressableData, data...) + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, sb, TypeSignedRational, TestDefaultByteOrder) + + value, err := vc.Values() + log.PanicIf(err) + + expected := []SignedRational{ + {Numerator: 1, Denominator: 2}, + {Numerator: 3, Denominator: 4}, + } + + if reflect.DeepEqual(value, expected) != true { + t.Fatalf("Values not correct (signed rationals): %v", value) + } +} diff --git a/common/value_encoder.go b/common/value_encoder.go new file mode 100644 index 0000000..2cd26cc --- /dev/null +++ b/common/value_encoder.go @@ -0,0 +1,273 @@ +package exifcommon + +import ( + "bytes" + "math" + "reflect" + "time" + + "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 +} + +// ValueEncoder knows how to encode values of every type to bytes. +type ValueEncoder struct { + byteOrder binary.ByteOrder +} + +// NewValueEncoder returns a new ValueEncoder. +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) encodeFloats(value []float32) (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], math.Float32bits(value[i])) + } + + ed.Type = TypeFloat + + return ed, nil +} + +func (ve *ValueEncoder) encodeDoubles(value []float64) (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.PutUint64(ed.Encoded[i*8:(i+1)*8], math.Float64bits(value[i])) + } + + ed.Type = TypeDouble + + 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)) + } + }() + + switch t := value.(type) { + case []byte: + ed, err = ve.encodeBytes(t) + log.PanicIf(err) + case string: + ed, err = ve.encodeAscii(t) + log.PanicIf(err) + case []uint16: + ed, err = ve.encodeShorts(t) + log.PanicIf(err) + case []uint32: + ed, err = ve.encodeLongs(t) + log.PanicIf(err) + case []float32: + ed, err = ve.encodeFloats(t) + log.PanicIf(err) + case []float64: + ed, err = ve.encodeDoubles(t) + log.PanicIf(err) + case []Rational: + ed, err = ve.encodeRationals(t) + log.PanicIf(err) + case []int32: + ed, err = ve.encodeSignedLongs(t) + log.PanicIf(err) + case []SignedRational: + ed, err = ve.encodeSignedRationals(t) + log.PanicIf(err) + case time.Time: + // For convenience, if the user doesn't want to deal with translation + // semantics with timestamps. + + s := ExifFullTimestampString(t) + + ed, err = ve.encodeAscii(s) + log.PanicIf(err) + default: + log.Panicf("value not encodable: [%s] [%v]", reflect.TypeOf(value), value) + } + + return ed, nil +} diff --git a/common/value_encoder_test.go b/common/value_encoder_test.go new file mode 100644 index 0000000..764920f --- /dev/null +++ b/common/value_encoder_test.go @@ -0,0 +1,725 @@ +package exifcommon + +import ( + "bytes" + "math" + "reflect" + "testing" + "time" + + "github.com/dsoprea/go-logging" +) + +func TestValueEncoder_encodeBytes__Cycle(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.") + } + + recovered, err := parser.ParseBytes(ed.Encoded, ed.UnitCount) + log.PanicIf(err) + + if reflect.DeepEqual(recovered, original) != true { + t.Fatalf("Value not recovered correctly.") + } +} + +func TestValueEncoder_encodeAscii__Cycle(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. + + recovered, err := parser.ParseAscii(ed.Encoded, ed.UnitCount) + log.PanicIf(err) + + if reflect.DeepEqual(recovered, original) != true { + t.Fatalf("Value not recovered correctly.") + } +} + +func TestValueEncoder_encodeAsciiNoNul__Cycle(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). + + recovered, err := parser.ParseAsciiNoNul(ed.Encoded, ed.UnitCount) + log.PanicIf(err) + + if reflect.DeepEqual(recovered, string(expected)) != true { + t.Fatalf("Value not recovered correctly.") + } +} + +func TestValueEncoder_encodeShorts__Cycle(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.") + } + + recovered, err := parser.ParseShorts(ed.Encoded, ed.UnitCount, byteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(recovered, original) != true { + t.Fatalf("Value not recovered correctly.") + } +} + +func TestValueEncoder_encodeLongs__Cycle(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.") + } + + recovered, err := parser.ParseLongs(ed.Encoded, ed.UnitCount, byteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(recovered, original) != true { + t.Fatalf("Value not recovered correctly.") + } +} + +func TestValueEncoder_encodeFloats__Cycle(t *testing.T) { + byteOrder := TestDefaultByteOrder + ve := NewValueEncoder(byteOrder) + + original := []float32{3.14159265, 2.71828182, 51.0, 68.0, 85.0} + + ed, err := ve.encodeFloats(original) + log.PanicIf(err) + + if ed.Type != TypeFloat { + t.Fatalf("IFD type not expected.") + } + + expected := []byte{ + 0x40, 0x49, 0x0f, 0xdb, + 0x40, 0x2d, 0xf8, 0x54, + 0x42, 0x4c, 0x00, 0x00, + 0x42, 0x88, 0x00, 0x00, + 0x42, 0xaa, 0x00, 0x00, + } + + if bytes.Equal(ed.Encoded, expected) != true { + t.Fatalf("Data not encoded correctly.") + } else if ed.UnitCount != 5 { + t.Fatalf("Unit-count not correct.") + } + + recovered, err := parser.ParseFloats(ed.Encoded, ed.UnitCount, byteOrder) + log.PanicIf(err) + + for i, v := range recovered { + if v < original[i] || v >= math.Nextafter32(original[i], original[i]+1) { + t.Fatalf("ReadFloats expecting %v, received %v", original[i], v) + } + } +} + +func TestValueEncoder_encodeDoubles__Cycle(t *testing.T) { + byteOrder := TestDefaultByteOrder + ve := NewValueEncoder(byteOrder) + + original := []float64{3.14159265, 2.71828182, 954877.1230695, 68.0, 85.0} + + ed, err := ve.encodeDoubles(original) + log.PanicIf(err) + + if ed.Type != TypeDouble { + t.Fatalf("IFD type not expected.") + } + + expected := []byte{ + 0x40, 0x09, 0x21, 0xfb, 0x53, 0xc8, 0xd4, 0xf1, + 0x40, 0x05, 0xbf, 0x0a, 0x89, 0xf1, 0xb0, 0xdd, + 0x41, 0x2d, 0x23, 0xfa, 0x3f, 0x02, 0xf7, 0x2b, + 0x40, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x55, 0x40, 0x00, 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.") + } + + recovered, err := parser.ParseDoubles(ed.Encoded, ed.UnitCount, byteOrder) + log.PanicIf(err) + + for i, v := range recovered { + if v < original[i] || v >= math.Nextafter(original[i], original[i]+1) { + t.Fatalf("ReadDoubles expecting %v, received %v", original[i], v) + } + } +} + +func TestValueEncoder_encodeRationals__Cycle(t *testing.T) { + byteOrder := TestDefaultByteOrder + ve := NewValueEncoder(byteOrder) + + original := []Rational{ + { + Numerator: 0x11, + Denominator: 0x22, + }, + { + Numerator: 0x33, + Denominator: 0x44, + }, + { + Numerator: 0x55, + Denominator: 0x66, + }, + { + Numerator: 0x77, + Denominator: 0x88, + }, + { + 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.") + } + + recovered, err := parser.ParseRationals(ed.Encoded, ed.UnitCount, byteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(recovered, original) != true { + t.Fatalf("Value not recovered correctly.") + } +} + +func TestValueEncoder_encodeSignedLongs__Cycle(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.") + } + + recovered, err := parser.ParseSignedLongs(ed.Encoded, ed.UnitCount, byteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(recovered, original) != true { + t.Fatalf("Value not recovered correctly.") + } +} + +func TestValueEncoder_encodeSignedRationals__Cycle(t *testing.T) { + byteOrder := TestDefaultByteOrder + ve := NewValueEncoder(byteOrder) + + original := []SignedRational{ + { + Numerator: 0x11, + Denominator: 0x22, + }, + { + Numerator: 0x33, + Denominator: 0x44, + }, + { + Numerator: 0x55, + Denominator: 0x66, + }, + { + Numerator: 0x77, + Denominator: 0x88, + }, + { + 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.") + } + + recovered, err := parser.ParseSignedRationals(ed.Encoded, ed.UnitCount, byteOrder) + log.PanicIf(err) + + if reflect.DeepEqual(recovered, original) != true { + t.Fatalf("Value not recovered correctly.") + } +} + +func TestValueEncoder_Encode__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 TestValueEncoder_Encode__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 TestValueEncoder_Encode__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 TestValueEncoder_Encode__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 TestValueEncoder_Encode__Float(t *testing.T) { + byteOrder := TestDefaultByteOrder + ve := NewValueEncoder(byteOrder) + + original := []float32{3.14159265, 2.71828182, 51.0, 68.0, 85.0} + + ed, err := ve.Encode(original) + log.PanicIf(err) + + if ed.Type != TypeFloat { + t.Fatalf("IFD type not expected.") + } + + expected := []byte{ + 0x40, 0x49, 0x0f, 0xdb, + 0x40, 0x2d, 0xf8, 0x54, + 0x42, 0x4c, 0x00, 0x00, + 0x42, 0x88, 0x00, 0x00, + 0x42, 0xaa, 0x00, 0x00, + } + + if bytes.Equal(ed.Encoded, expected) != true { + t.Fatalf("Data not encoded correctly.") + } else if ed.UnitCount != 5 { + t.Fatalf("Unit-count not correct.") + } + +} + +func TestValueEncoder_Encode__Double(t *testing.T) { + byteOrder := TestDefaultByteOrder + ve := NewValueEncoder(byteOrder) + + original := []float64{3.14159265, 2.71828182, 954877.1230695, 68.0, 85.0} + + ed, err := ve.Encode(original) + log.PanicIf(err) + + if ed.Type != TypeDouble { + t.Fatalf("IFD type not expected.") + } + + expected := []byte{ + 0x40, 0x09, 0x21, 0xfb, 0x53, 0xc8, 0xd4, 0xf1, + 0x40, 0x05, 0xbf, 0x0a, 0x89, 0xf1, 0xb0, 0xdd, + 0x41, 0x2d, 0x23, 0xfa, 0x3f, 0x02, 0xf7, 0x2b, + 0x40, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x55, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + } + + if bytes.Equal(ed.Encoded, expected) != true { + t.Fatalf("Data not encoded correctly.") + } else if ed.UnitCount != 5 { + t.Fatalf("Unit-count not correct.") + } + +} + +func TestValueEncoder_Encode__Rational(t *testing.T) { + byteOrder := TestDefaultByteOrder + ve := NewValueEncoder(byteOrder) + + original := []Rational{ + { + Numerator: 0x11, + Denominator: 0x22, + }, + { + Numerator: 0x33, + Denominator: 0x44, + }, + { + Numerator: 0x55, + Denominator: 0x66, + }, + { + Numerator: 0x77, + Denominator: 0x88, + }, + { + 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 TestValueEncoder_Encode__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 TestValueEncoder_Encode__SignedRational(t *testing.T) { + byteOrder := TestDefaultByteOrder + ve := NewValueEncoder(byteOrder) + + original := []SignedRational{ + { + Numerator: 0x11, + Denominator: 0x22, + }, + { + Numerator: 0x33, + Denominator: 0x44, + }, + { + Numerator: 0x55, + Denominator: 0x66, + }, + { + Numerator: 0x77, + Denominator: 0x88, + }, + { + 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.") + } +} + +func TestValueEncoder_Encode__Timestamp(t *testing.T) { + byteOrder := TestDefaultByteOrder + ve := NewValueEncoder(byteOrder) + + now := time.Now() + + ed, err := ve.Encode(now) + log.PanicIf(err) + + if ed.Type != TypeAscii { + t.Fatalf("Timestamp not encoded as ASCII.") + } + + expectedTimestampBytes := ExifFullTimestampString(now) + + // Leave an extra byte for the NUL. + expected := make([]byte, len(expectedTimestampBytes)+1) + copy(expected, expectedTimestampBytes) + + if bytes.Equal(ed.Encoded, expected) != true { + t.Fatalf("Timestamp not encoded correctly: [%s] != [%s]", string(ed.Encoded), string(expected)) + } +} diff --git a/data_layer.go b/data_layer.go new file mode 100644 index 0000000..7883752 --- /dev/null +++ b/data_layer.go @@ -0,0 +1,50 @@ +package exif + +import ( + "io" + + "github.com/dsoprea/go-logging" + "github.com/dsoprea/go-utility/v2/filesystem" +) + +type ExifBlobSeeker interface { + GetReadSeeker(initialOffset int64) (rs io.ReadSeeker, err error) +} + +// ExifReadSeeker knows how to retrieve data from the EXIF blob relative to the +// beginning of the blob (so, absolute position (0) is the first byte of the +// EXIF data). +type ExifReadSeeker struct { + rs io.ReadSeeker +} + +func NewExifReadSeeker(rs io.ReadSeeker) *ExifReadSeeker { + return &ExifReadSeeker{ + rs: rs, + } +} + +func NewExifReadSeekerWithBytes(exifData []byte) *ExifReadSeeker { + sb := rifs.NewSeekableBufferWithBytes(exifData) + edbs := NewExifReadSeeker(sb) + + return edbs +} + +// Fork creates a new ReadSeeker instead that wraps a BouncebackReader to +// maintain its own position in the stream. +func (edbs *ExifReadSeeker) GetReadSeeker(initialOffset int64) (rs io.ReadSeeker, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + br, err := rifs.NewBouncebackReader(edbs.rs) + log.PanicIf(err) + + _, err = br.Seek(initialOffset, io.SeekStart) + log.PanicIf(err) + + return br, nil +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..2f00b08 --- /dev/null +++ b/error.go @@ -0,0 +1,14 @@ +package exif + +import ( + "errors" +) + +var ( + // ErrTagNotFound indicates that the tag was not found. + ErrTagNotFound = errors.New("tag not found") + + // ErrTagNotKnown indicates that the tag is not registered with us as a + // known tag. + ErrTagNotKnown = errors.New("tag is not known") +) diff --git a/exif.go b/exif.go new file mode 100644 index 0000000..049625a --- /dev/null +++ b/exif.go @@ -0,0 +1,333 @@ +package exif + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "os" + + "encoding/binary" + "io/ioutil" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +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) +) + +const ( + // ExifSignatureLength is the number of bytes in the EXIF signature (which + // customarily includes the first IFD offset). + ExifSignatureLength = 8 +) + +var ( + exifLogger = log.NewLogger("exif.exif") + + ExifBigEndianSignature = [4]byte{'M', 'M', 0x00, 0x2a} + ExifLittleEndianSignature = [4]byte{'I', 'I', 0x2a, 0x00} +) + +var ( + ErrNoExif = errors.New("no exif data") + ErrExifHeaderError = errors.New("exif header error") +) + +// SearchAndExtractExif searches for an EXIF blob in the byte-slice. +func SearchAndExtractExif(data []byte) (rawExif []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + b := bytes.NewBuffer(data) + + rawExif, err = SearchAndExtractExifWithReader(b) + if err != nil { + if err == ErrNoExif { + return nil, err + } + + log.Panic(err) + } + + return rawExif, nil +} + +// SearchAndExtractExifN searches for an EXIF blob in the byte-slice, but skips +// the given number of EXIF blocks first. This is a forensics tool that helps +// identify multiple EXIF blocks in a file. +func SearchAndExtractExifN(data []byte, n int) (rawExif []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + skips := 0 + totalDiscarded := 0 + for { + b := bytes.NewBuffer(data) + + var discarded int + + rawExif, discarded, err = searchAndExtractExifWithReaderWithDiscarded(b) + if err != nil { + if err == ErrNoExif { + return nil, err + } + + log.Panic(err) + } + + exifLogger.Debugf(nil, "Read EXIF block (%d).", skips) + + totalDiscarded += discarded + + if skips >= n { + exifLogger.Debugf(nil, "Reached requested EXIF block (%d).", n) + break + } + + nextOffset := discarded + 1 + exifLogger.Debugf(nil, "Skipping EXIF block (%d) by seeking to position (%d).", skips, nextOffset) + + data = data[nextOffset:] + skips++ + } + + exifLogger.Debugf(nil, "Found EXIF blob (%d) bytes from initial position.", totalDiscarded) + return rawExif, nil +} + +// searchAndExtractExifWithReaderWithDiscarded searches for an EXIF blob using +// an `io.Reader`. We can't know how much long the EXIF data is without parsing +// it, so this will likely grab up a lot of the image-data, too. +// +// This function returned the count of preceding bytes. +func searchAndExtractExifWithReaderWithDiscarded(r io.Reader) (rawExif []byte, discarded int, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // Search for the beginning of the EXIF information. The EXIF is near the + // beginning of most JPEGs, so this likely doesn't have a high cost (at + // least, again, with JPEGs). + + br := bufio.NewReader(r) + + for { + window, err := br.Peek(ExifSignatureLength) + if err != nil { + if err == io.EOF { + return nil, 0, ErrNoExif + } + + log.Panic(err) + } + + _, err = ParseExifHeader(window) + if err != nil { + if log.Is(err, ErrNoExif) == true { + // No EXIF. Move forward by one byte. + + _, err := br.Discard(1) + log.PanicIf(err) + + discarded++ + + continue + } + + // Some other error. + log.Panic(err) + } + + break + } + + exifLogger.Debugf(nil, "Found EXIF blob (%d) bytes from initial position.", discarded) + + rawExif, err = ioutil.ReadAll(br) + log.PanicIf(err) + + return rawExif, discarded, nil +} + +// RELEASE(dustin): We should replace the implementation of SearchAndExtractExifWithReader with searchAndExtractExifWithReaderWithDiscarded and drop the latter. + +// SearchAndExtractExifWithReader searches for an EXIF blob using an +// `io.Reader`. We can't know how much long the EXIF data is without parsing it, +// so this will likely grab up a lot of the image-data, too. +func SearchAndExtractExifWithReader(r io.Reader) (rawExif []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rawExif, _, err = searchAndExtractExifWithReaderWithDiscarded(r) + if err != nil { + if err == ErrNoExif { + return nil, err + } + + log.Panic(err) + } + + return rawExif, 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)) + } + }() + + // Open the file. + + f, err := os.Open(filepath) + log.PanicIf(err) + + defer f.Close() + + rawExif, err = SearchAndExtractExifWithReader(f) + log.PanicIf(err) + + return rawExif, nil +} + +type ExifHeader struct { + ByteOrder binary.ByteOrder + FirstIfdOffset uint32 +} + +func (eh ExifHeader) String() string { + return fmt.Sprintf("ExifHeader", 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) < ExifSignatureLength { + exifLogger.Warningf(nil, "Not enough data for EXIF header: (%d)", len(data)) + return eh, ErrNoExif + } + + if bytes.Equal(data[:4], ExifBigEndianSignature[:]) == true { + exifLogger.Debugf(nil, "Byte-order is big-endian.") + eh.ByteOrder = binary.BigEndian + } else if bytes.Equal(data[:4], ExifLittleEndianSignature[:]) == true { + eh.ByteOrder = binary.LittleEndian + exifLogger.Debugf(nil, "Byte-order is little-endian.") + } else { + return eh, ErrNoExif + } + + eh.FirstIfdOffset = eh.ByteOrder.Uint32(data[4:8]) + + return eh, nil +} + +// Visit recursively invokes a callback for every tag. +func Visit(rootIfdIdentity *exifcommon.IfdIdentity, ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, exifData []byte, visitor TagVisitorFn, so *ScanOptions) (eh ExifHeader, furthestOffset uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + eh, err = ParseExifHeader(exifData) + log.PanicIf(err) + + ebs := NewExifReadSeekerWithBytes(exifData) + ie := NewIfdEnumerate(ifdMapping, tagIndex, ebs, eh.ByteOrder) + + _, err = ie.Scan(rootIfdIdentity, eh.FirstIfdOffset, visitor, so) + log.PanicIf(err) + + furthestOffset = ie.FurthestOffset() + + return eh, furthestOffset, nil +} + +// Collect recursively builds a static structure of all IFDs and tags. +func Collect(ifdMapping *exifcommon.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) + + ebs := NewExifReadSeekerWithBytes(exifData) + ie := NewIfdEnumerate(ifdMapping, tagIndex, ebs, eh.ByteOrder) + + index, err = ie.Collect(eh.FirstIfdOffset) + log.PanicIf(err) + + return eh, index, nil +} + +// BuildExifHeader constructs the bytes that go at the front of the stream. +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) + + var signatureBytes []byte + if byteOrder == binary.BigEndian { + signatureBytes = ExifBigEndianSignature[:] + } else { + signatureBytes = ExifLittleEndianSignature[:] + } + + _, err = b.Write(signatureBytes) + log.PanicIf(err) + + err = binary.Write(b, byteOrder, firstIfdOffset) + log.PanicIf(err) + + return b.Bytes(), nil +} diff --git a/exif_test.go b/exif_test.go new file mode 100644 index 0000000..fc932d1 --- /dev/null +++ b/exif_test.go @@ -0,0 +1,422 @@ +package exif + +import ( + "bytes" + "fmt" + "os" + "reflect" + "sort" + "testing" + + "encoding/binary" + "io/ioutil" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +func TestVisit(t *testing.T) { + defer func() { + if state := recover(); state != nil { + err := log.Wrap(state.(error)) + log.PrintError(err) + + t.Fatalf("Test failure.") + } + }() + + ti := NewTagIndex() + + // Open the file. + + testImageFilepath := getTestImageFilepath() + + 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. + + // TODO(dustin): Just use SearchAndExtractExifWithReader() here. + + 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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + tags := make([]string, 0) + + // DEPRECATED(dustin): fqIfdPath and ifdIndex are now redundant. Remove in next module version. + visitor := func(ite *IfdTagEntry) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + log.Panic(err) + } + }() + + tagId := ite.TagId() + tagType := ite.TagType() + ii := ite.ifdIdentity + + it, err := ti.Get(ii, tagId) + if err != nil { + if log.Is(err, ErrTagNotFound) { + fmt.Printf("Unknown tag: [%s] (%04x)\n", ii.String(), tagId) + return nil + } + + log.Panic(err) + } + + valueString, err := ite.FormatFirst() + log.PanicIf(err) + + description := fmt.Sprintf("IFD-PATH=[%s] ID=(0x%04x) NAME=[%s] COUNT=(%d) TYPE=[%s] VALUE=[%s]", ii.String(), tagId, it.Name, ite.UnitCount(), tagType.String(), valueString) + tags = append(tags, description) + + return nil + } + + _, furthestOffset, err := Visit(exifcommon.IfdStandardIfdIdentity, im, ti, data[foundAt:], visitor, nil) + log.PanicIf(err) + + if furthestOffset != 32935 { + t.Fatalf("Furthest-offset is not valid: (%d)", furthestOffset) + } + + 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=[Exif9101ComponentsConfiguration]", + "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]", + "IFD-PATH=[IFD/Exif] ID=(0x9286) NAME=[UserComment] COUNT=(264) TYPE=[UNDEFINED] VALUE=[UserComment]", + "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=[IFD1] ID=(0x0103) NAME=[Compression] COUNT=(1) TYPE=[SHORT] VALUE=[6]", + "IFD-PATH=[IFD1] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]", + "IFD-PATH=[IFD1] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]", + "IFD-PATH=[IFD1] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]", + "IFD-PATH=[IFD1] ID=(0x0201) NAME=[JPEGInterchangeFormat] COUNT=(1) TYPE=[LONG] VALUE=[11444]", + "IFD-PATH=[IFD1] ID=(0x0202) NAME=[JPEGInterchangeFormatLength] COUNT=(1) TYPE=[LONG] VALUE=[21491]", + } + + 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) { + testImageFilepath := getTestImageFilepath() + + // Returns a slice starting with the EXIF data and going to the end of the + // image. + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + testExifData := getTestExifData() + + if bytes.Compare(rawExif[:len(testExifData)], testExifData) != 0 { + t.Fatalf("found EXIF data not correct") + } +} + +func TestSearchAndExtractExif(t *testing.T) { + testImageFilepath := getTestImageFilepath() + + imageData, err := ioutil.ReadFile(testImageFilepath) + log.PanicIf(err) + + rawExif, err := SearchAndExtractExif(imageData) + log.PanicIf(err) + + testExifData := getTestExifData() + + if bytes.Compare(rawExif[:len(testExifData)], testExifData) != 0 { + t.Fatalf("found EXIF data not correct") + } +} + +func TestSearchAndExtractExifWithReader(t *testing.T) { + testImageFilepath := getTestImageFilepath() + + f, err := os.Open(testImageFilepath) + log.PanicIf(err) + + defer f.Close() + + rawExif, err := SearchAndExtractExifWithReader(f) + log.PanicIf(err) + + testExifData := getTestExifData() + + 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.PrintError(err) + + t.Fatalf("Test failure.") + } + }() + + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + 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)) + } + + actualIfdPaths := make([]string, len(lookup)) + i := 0 + for ifdPath := range lookup { + actualIfdPaths[i] = ifdPath + i++ + } + + sort.Strings(actualIfdPaths) + + expectedIfdPaths := []string{ + "IFD", + "IFD/Exif", + "IFD/Exif/Iop", + "IFD/GPSInfo", + "IFD1", + } + + if reflect.DeepEqual(actualIfdPaths, expectedIfdPaths) != true { + t.Fatalf("The IFD lookup is not the right size: %v", actualIfdPaths) + } + + 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.ifdIdentity.UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() { + t.Fatalf("Root IFD is not labeled correctly: [%s]", rootIfd.ifdIdentity.UnindexedString()) + } else if rootIfd.nextIfd.ifdIdentity.UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() { + t.Fatalf("Root IFD sibling is not labeled correctly: [%s]", rootIfd.ifdIdentity.UnindexedString()) + } else if rootIfd.Children()[0].ifdIdentity.UnindexedString() != exifcommon.IfdExifStandardIfdIdentity.UnindexedString() { + t.Fatalf("Root IFD child (0) is not labeled correctly: [%s]", rootIfd.Children()[0].ifdIdentity.UnindexedString()) + } else if rootIfd.Children()[1].ifdIdentity.UnindexedString() != exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString() { + t.Fatalf("Root IFD child (1) is not labeled correctly: [%s]", rootIfd.Children()[1].ifdIdentity.UnindexedString()) + } else if rootIfd.Children()[0].children[0].ifdIdentity.UnindexedString() != exifcommon.IfdExifIopStandardIfdIdentity.UnindexedString() { + t.Fatalf("Exif IFD child is not an IOP IFD: [%s]", rootIfd.Children()[0].children[0].ifdIdentity.UnindexedString()) + } + + if lookup[exifcommon.IfdStandardIfdIdentity.UnindexedString()].ifdIdentity.UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() { + t.Fatalf("Lookup for standard IFD not correct.") + } else if lookup[exifcommon.IfdStandardIfdIdentity.UnindexedString()+"1"].ifdIdentity.UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() { + t.Fatalf("Lookup for standard IFD not correct.") + } + + if lookup[exifcommon.IfdExifStandardIfdIdentity.UnindexedString()].ifdIdentity.UnindexedString() != exifcommon.IfdExifStandardIfdIdentity.UnindexedString() { + t.Fatalf("Lookup for EXIF IFD not correct.") + } + + if lookup[exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString()].ifdIdentity.UnindexedString() != exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString() { + t.Fatalf("Lookup for GPS IFD not correct.") + } + + if lookup[exifcommon.IfdExifIopStandardIfdIdentity.UnindexedString()].ifdIdentity.UnindexedString() != exifcommon.IfdExifIopStandardIfdIdentity.UnindexedString() { + t.Fatalf("Lookup for IOP IFD not correct.") + } + + foundExif := 0 + foundGps := 0 + for _, ite := range lookup[exifcommon.IfdStandardIfdIdentity.UnindexedString()].entries { + if ite.ChildIfdPath() == exifcommon.IfdExifStandardIfdIdentity.UnindexedString() { + foundExif++ + + if ite.TagId() != exifcommon.IfdExifStandardIfdIdentity.TagId() { + t.Fatalf("EXIF IFD tag-ID mismatch: (0x%04x) != (0x%04x)", ite.TagId(), exifcommon.IfdExifStandardIfdIdentity.TagId()) + } + } + + if ite.ChildIfdPath() == exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString() { + foundGps++ + + if ite.TagId() != exifcommon.IfdGpsInfoStandardIfdIdentity.TagId() { + t.Fatalf("GPS IFD tag-ID mismatch: (0x%04x) != (0x%04x)", ite.TagId(), exifcommon.IfdGpsInfoStandardIfdIdentity.TagId()) + } + } + } + + 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[exifcommon.IfdExifStandardIfdIdentity.UnindexedString()].entries { + if ite.ChildIfdPath() == exifcommon.IfdExifIopStandardIfdIdentity.UnindexedString() { + foundIop++ + + if ite.TagId() != exifcommon.IfdExifIopStandardIfdIdentity.TagId() { + t.Fatalf("IOP IFD tag-ID mismatch: (0x%04x) != (0x%04x)", ite.TagId(), exifcommon.IfdExifIopStandardIfdIdentity.TagId()) + } + } + } + + if foundIop != 1 { + t.Fatalf("Exactly one IOP IFD tag wasn't found: (%d)", foundIop) + } +} + +func TestParseExifHeader(t *testing.T) { + testExifData := getTestExifData() + + 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(exifcommon.TestDefaultByteOrder, 0x11223344) + log.PanicIf(err) + + eh, err := ParseExifHeader(headerBytes) + log.PanicIf(err) + + if eh.ByteOrder != exifcommon.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(exifcommon.TestDefaultByteOrder, 0x11223344) + log.PanicIf(err) + + eh, err := ParseExifHeader(headerBytes) + log.PanicIf(err) + + fmt.Printf("%v\n", eh) + + // Output: ExifHeader +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7200c86 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module b612.me/exif + +go 1.12 + +// Development only +// replace github.com/dsoprea/go-logging => ../../go-logging +// replace github.com/dsoprea/go-utility/v2 => ../../go-utility/v2 + +require ( + github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd + github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 + github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 + github.com/jessevdk/go-flags v1.5.0 + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4791437 --- /dev/null +++ b/go.sum @@ -0,0 +1,52 @@ +github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E= +github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8= +github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk= +github.com/dsoprea/go-exif/v3 v3.0.0-20221003160559-cf5cd88aa559/go.mod h1:rW6DMEv25U9zCtE5ukC7ttBRllXj7g7TAHl7tQrT5No= +github.com/dsoprea/go-exif/v3 v3.0.0-20221003171958-de6cb6e380a8/go.mod h1:akyZEJZ/k5bmbC9gA612ZLQkcED8enS9vuTiuAkENr0= +github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA= +github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= +github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg= +github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= +github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf h1:/w4QxepU4AHh3AuO6/g8y/YIIHH5+aKP3Bj8sg5cqhU= +github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= +github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU= +github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LVjRU0RNUuMDqkPTxcALio0LWPFPXxxFCvVGVAwEpFc= +github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU= +github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw= +github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= +github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo= +github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b h1:6e93nYa3hNqAvLr0pD4PN1fFS+gKzp2zAXqrnTCstqU= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/gps.go b/gps.go new file mode 100644 index 0000000..8b75f20 --- /dev/null +++ b/gps.go @@ -0,0 +1,117 @@ +package exif + +import ( + "errors" + "fmt" + "time" + + "github.com/dsoprea/go-logging" + "github.com/golang/geo/s2" + + "b612.me/exif/common" +) + +var ( + // ErrGpsCoordinatesNotValid means that some part of the geographic data was + // unparseable. + ErrGpsCoordinatesNotValid = errors.New("GPS coordinates not valid") +) + +// GpsDegrees is a high-level struct representing geographic data. +type GpsDegrees struct { + // Orientation describes the N/E/S/W direction that this position is + // relative to. + Orientation byte + + // Degrees is a simple float representing the underlying rational degrees + // amount. + Degrees float64 + + // Minutes is a simple float representing the underlying rational minutes + // amount. + Minutes float64 + + // Seconds is a simple float representing the underlying ration seconds + // amount. + Seconds float64 +} + +// NewGpsDegreesFromRationals returns a GpsDegrees struct given the EXIF-encoded +// information. The refValue is the N/E/S/W direction that this position is +// relative to. +func NewGpsDegreesFromRationals(refValue string, rawCoordinate []exifcommon.Rational) (gd GpsDegrees, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if len(rawCoordinate) != 3 { + log.Panicf("new GpsDegrees struct requires a raw-coordinate with exactly three rationals") + } + + gd = GpsDegrees{ + Orientation: refValue[0], + Degrees: float64(rawCoordinate[0].Numerator) / float64(rawCoordinate[0].Denominator), + Minutes: float64(rawCoordinate[1].Numerator) / float64(rawCoordinate[1].Denominator), + Seconds: float64(rawCoordinate[2].Numerator) / float64(rawCoordinate[2].Denominator), + } + + return gd, nil +} + +// String provides returns a descriptive string. +func (d GpsDegrees) String() string { + return fmt.Sprintf("Degrees", string([]byte{d.Orientation}), d.Degrees, d.Minutes, d.Seconds) +} + +// Decimal calculates and returns the simplified float representation of the +// component degrees. +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 + } + + return decimal +} + +// Raw returns a Rational struct that can be used to *write* coordinates. In +// practice, the denominator are typically (1) in the original EXIF data, and, +// that being the case, this will best preserve precision. +func (d GpsDegrees) Raw() []exifcommon.Rational { + return []exifcommon.Rational{ + {Numerator: uint32(d.Degrees), Denominator: 1}, + {Numerator: uint32(d.Minutes), Denominator: 1}, + {Numerator: uint32(d.Seconds), Denominator: 1}, + } +} + +// GpsInfo encapsulates all of the geographic information in one place. +type GpsInfo struct { + Latitude, Longitude GpsDegrees + Altitude int + Timestamp time.Time +} + +// String returns a descriptive string. +func (gi *GpsInfo) String() string { + return fmt.Sprintf("GpsInfo", + gi.Latitude.Decimal(), gi.Longitude.Decimal(), gi.Altitude, gi.Timestamp) +} + +// S2CellId returns the cell-ID of the geographic location on the earth. +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/gps_test.go b/gps_test.go new file mode 100644 index 0000000..ffba121 --- /dev/null +++ b/gps_test.go @@ -0,0 +1,61 @@ +package exif + +import ( + "math" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +func TestNewGpsDegreesFromRationals(t *testing.T) { + latitudeRaw := []exifcommon.Rational{ + {Numerator: 22, Denominator: 2}, + {Numerator: 66, Denominator: 3}, + {Numerator: 132, Denominator: 4}, + } + + gd, err := NewGpsDegreesFromRationals("W", latitudeRaw) + log.PanicIf(err) + + if gd.Orientation != 'W' { + t.Fatalf("Orientation was not set correctly: [%s]", string([]byte{gd.Orientation})) + } + + degreesRightBound := math.Nextafter(11.0, 12.0) + minutesRightBound := math.Nextafter(22.0, 23.0) + secondsRightBound := math.Nextafter(33.0, 34.0) + + if gd.Degrees < 11.0 || gd.Degrees >= degreesRightBound { + t.Fatalf("Degrees is not correct: (%.2f)", gd.Degrees) + } else if gd.Minutes < 22.0 || gd.Minutes >= minutesRightBound { + t.Fatalf("Minutes is not correct: (%.2f)", gd.Minutes) + } else if gd.Seconds < 33.0 || gd.Seconds >= secondsRightBound { + t.Fatalf("Seconds is not correct: (%.2f)", gd.Seconds) + } +} + +func TestGpsDegrees_Raw(t *testing.T) { + latitudeRaw := []exifcommon.Rational{ + {Numerator: 22, Denominator: 2}, + {Numerator: 66, Denominator: 3}, + {Numerator: 132, Denominator: 4}, + } + + gd, err := NewGpsDegreesFromRationals("W", latitudeRaw) + log.PanicIf(err) + + actual := gd.Raw() + + expected := []exifcommon.Rational{ + {Numerator: 11, Denominator: 1}, + {Numerator: 22, Denominator: 1}, + {Numerator: 33, Denominator: 1}, + } + + if reflect.DeepEqual(actual, expected) != true { + t.Fatalf("GpsInfo not correctly encoded down to raw: %v\n", actual) + } +} diff --git a/ifd_builder.go b/ifd_builder.go new file mode 100644 index 0000000..cfabd9d --- /dev/null +++ b/ifd_builder.go @@ -0,0 +1,1211 @@ +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" + + "b612.me/exif/common" + "b612.me/exif/undefined" +) + +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", valuePhrase, len(ibtv.valueBytes)) + } else if ibtv.IsIb() == true { + return fmt.Sprintf("IfdBuilderTagValue", 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 exifcommon.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 (bt *BuilderTag) TagId() uint16 { + return bt.tagId +} + +func (bt *BuilderTag) TypeId() exifcommon.TagTypePrimitive { + return bt.typeId +} + +func (bt *BuilderTag) ByteOrder() binary.ByteOrder { + return bt.byteOrder +} + +func NewBuilderTag(ifdPath string, tagId uint16, typeId exifcommon.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: exifcommon.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 = exifcommon.FormatFromBytes(bt.value.Bytes(), bt.typeId, false, bt.byteOrder) + log.PanicIf(err) + } else { + valueString = fmt.Sprintf("%v", bt.value) + } + + return fmt.Sprintf("BuilderTag", bt.ifdPath, bt.tagId, bt.typeId.String(), 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. + + var ed exifcommon.EncodedData + if bt.typeId == exifcommon.TypeUndefined { + encodeable := value.(exifundefined.EncodeableValue) + + encoded, unitCount, err := exifundefined.Encode(encodeable, byteOrder) + log.PanicIf(err) + + ed = exifcommon.EncodedData{ + Type: exifcommon.TypeUndefined, + Encoded: encoded, + UnitCount: unitCount, + } + } else { + ve := exifcommon.NewValueEncoder(byteOrder) + + var err error + + ed, err = ve.Encode(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 { + // If there is more than one supported type, we'll go with the larger to + // encode with. It'll use the same amount of fixed-space, and we'll + // eliminate unnecessary overflows/issues. + tagType := it.GetEncodingType(value) + + var rawBytes []byte + if it.DoesSupportType(exifcommon.TypeUndefined) == true { + encodeable := value.(exifundefined.EncodeableValue) + + var err error + + rawBytes, _, err = exifundefined.Encode(encodeable, byteOrder) + log.PanicIf(err) + } else { + ve := exifcommon.NewValueEncoder(byteOrder) + + ed, err := ve.Encode(value) + log.PanicIf(err) + + rawBytes = ed.Encoded + } + + tagValue := NewIfdBuilderTagValueFromBytes(rawBytes) + + return NewBuilderTag( + ifdPath, + it.Id, + tagType, + tagValue, + byteOrder) +} + +type IfdBuilder struct { + ifdIdentity *exifcommon.IfdIdentity + + 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 *exifcommon.IfdMapping + tagIndex *TagIndex +} + +func NewIfdBuilder(ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, ii *exifcommon.IfdIdentity, byteOrder binary.ByteOrder) (ib *IfdBuilder) { + ib = &IfdBuilder{ + ifdIdentity: ii, + + 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) { + ib = &IfdBuilder{ + ifdIdentity: ifd.IfdIdentity(), + + 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) (firstIb *IfdBuilder) { + var lastIb *IfdBuilder + i := 0 + for thisExistingIfd := rootIfd; thisExistingIfd != nil; thisExistingIfd = thisExistingIfd.nextIfd { + newIb := NewIfdBuilder( + rootIfd.ifdMapping, + rootIfd.tagIndex, + rootIfd.ifdIdentity, + thisExistingIfd.ByteOrder()) + + if firstIb == nil { + firstIb = newIb + } else { + lastIb.SetNextIb(newIb) + } + + err := newIb.AddTagsFromExisting(thisExistingIfd, nil, nil) + log.PanicIf(err) + + lastIb = newIb + i++ + } + + return firstIb +} + +func (ib *IfdBuilder) IfdIdentity() *exifcommon.IfdIdentity { + return ib.ifdIdentity +} + +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.IfdIdentity().TagId() == childIfdTagId { + return childIbThis, nil + } + } + + log.Panic(ErrChildIbNotFound) + + // Never reached. + return nil, nil +} + +func getOrCreateIbFromRootIbInner(rootIb *IfdBuilder, parentIb *IfdBuilder, currentLineage []exifcommon.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 []exifcommon.IfdTagIdAndIndex + if parentIb != nil { + var err error + + parentLineage, err = thisIb.ifdMapping.ResolvePath(parentIb.IfdIdentity().String()) + 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.IfdIdentity().String() { + log.Panicf("the FQ IFD-path [%s] we were given does not match the builder's FQ IFD-path [%s]", expectedFqRootIfdPath, thisIb.IfdIdentity().String()) + } + + // 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. + + iiSibling := thisIb.IfdIdentity().NewSibling(i + 1) + thisIb.nextIb = NewIfdBuilder(thisIb.ifdMapping, thisIb.tagIndex, iiSibling, 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.IfdIdentity().TagId() == childItii.TagId { + foundChild = childIb + break + } + } + + // If we didn't find the child, add it. + + if foundChild == nil { + currentIfdTag := thisIb.IfdIdentity().IfdTag() + + childIfdTag := + exifcommon.NewIfdTag( + ¤tIfdTag, + childItii.TagId, + childItii.Name) + + iiChild := thisIb.IfdIdentity().NewChild(childIfdTag, 0) + + foundChild = + NewIfdBuilder( + thisIb.ifdMapping, + thisIb.tagIndex, + iiChild, + 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.IfdIdentity().UnindexedString() + } + + return fmt.Sprintf("IfdBuilder", ib.IfdIdentity().UnindexedString(), ib.IfdIdentity().TagId(), 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.IfdIdentity().UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() { + 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.IfdIdentity().UnindexedString(), + ThumbnailOffsetTagId, + exifcommon.TypeLong, + ibtvfb, + ib.byteOrder) + + err = ib.Set(offsetBt) + log.PanicIf(err) + + thumbnailSizeIt, err := ib.tagIndex.Get(ib.IfdIdentity(), ThumbnailSizeTagId) + log.PanicIf(err) + + sizeBt := NewStandardBuilderTag(ib.IfdIdentity().UnindexedString(), 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.IfdIdentity().UnindexedString(), tag.tagId) + if err == nil { + isChildIb = true + } else if log.Is(err, exifcommon.ErrChildIfdNotMapped) == false { + log.Panic(err) + } + + tagName := "" + + // If a normal tag (not a child IFD) get the name. + if isChildIb == true { + tagName = "" + } else { + it, err := ib.tagIndex.Get(ib.ifdIdentity, tag.tagId) + if log.Is(err, ErrTagNotFound) == true { + tagName = "" + } 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.IfdIdentity().UnindexedString(), tag.tagId) + if err == nil { + isChildIb = true + } else if log.Is(err, exifcommon.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", prefix, thisIb.IfdIdentity().String(), siblingIfdIndex, thisIb.IfdIdentity().TagId(), 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.IfdIdentity().UnindexedString() + } + + line := fmt.Sprintf("TAG", prefix, thisIb.IfdIdentity().String(), thisIb.IfdIdentity().TagId(), childIfdName, i, tag.tagId) + linesOutput = append(linesOutput, line) + + if childIb == nil { + continue + } + + childPrefix := "" + if prefix == "" { + childPrefix = fmt.Sprintf("%s", thisIb.IfdIdentity().UnindexedString()) + } else { + childPrefix = fmt.Sprintf("%s->%s", prefix, thisIb.IfdIdentity().UnindexedString()) + } + + 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.IfdIdentity(), 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.IfdIdentity().TagId() == 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 occur`ring 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.IfdIdentity().TagId() { + log.Panicf("child-IFD already added: %v", childIb.IfdIdentity().UnindexedString()) + } + } + + 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.IfdIdentity().UnindexedString(), + childIb.IfdIdentity().TagId(), + 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, includeTagIds []uint16, excludeTagIds []uint16) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + 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.IsThumbnailOffset() == true || ite.IsThumbnailSize() { + // 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.ifdIdentity.TagId() != 0xffff && thisChildIfd.ifdIdentity.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.ifdIdentity.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) + bt = ib.NewBuilderTagFromBuilder(childIb) + } else { + // Non-IFD tag. + + rawBytes, err := ite.GetRawBytes() + log.PanicIf(err) + + value := NewIfdBuilderTagValueFromBytes(rawBytes) + + bt = NewBuilderTag( + ifd.ifdIdentity.UnindexedString(), + 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.IfdIdentity(), tagId) + log.PanicIf(err) + + bt := NewStandardBuilderTag(ib.IfdIdentity().UnindexedString(), 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.IfdIdentity(), tagName) + log.PanicIf(err) + + bt := NewStandardBuilderTag(ib.IfdIdentity().UnindexedString(), 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.IfdIdentity(), tagId) + log.PanicIf(err) + + bt := NewStandardBuilderTag(ib.IfdIdentity().UnindexedString(), 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.IfdIdentity(), tagName) + log.PanicIf(err) + + bt := NewStandardBuilderTag(ib.IfdIdentity().UnindexedString(), 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/ifd_builder_encode.go b/ifd_builder_encode.go new file mode 100644 index 0000000..ea7ba38 --- /dev/null +++ b/ifd_builder_encode.go @@ -0,0 +1,536 @@ +package exif + +import ( + "bytes" + "fmt" + "strings" + + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +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 == exifcommon.TypeUndefined { + effectiveType = exifcommon.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) + } + } + if bt.tagId == ThumbnailOffsetTagId { + // The thumbnail offset is store as a long and its unit count must be 1 + unitCount = 1 + } + + 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.IfdIdentity().UnindexedString(), bt.value.Ib().IfdIdentity().UnindexedString()) + + // 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().IfdIdentity().UnindexedString(), ib.IfdIdentity().UnindexedString()) + + // 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().IfdIdentity().UnindexedString()) + + 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.IfdIdentity().UnindexedString()) + + ibe.pushToJournal("encodeAndAttachIfd", ">", "Calculating size: (%d) [%s]", i, thisIb.IfdIdentity().UnindexedString()) + + _, tableSize, allocatedDataSize, _, err := ibe.encodeIfdToBytes(thisIb, ifdAddressableOffset, 0, false) + log.PanicIf(err) + + ibe.pushToJournal("encodeAndAttachIfd", "<", "Finished calculating size: (%d) [%s]", i, thisIb.IfdIdentity().UnindexedString()) + + 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.IfdIdentity().UnindexedString(), 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.IfdIdentity().UnindexedString()) + + 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.IfdIdentity().UnindexedString(), 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/ifd_builder_encode_test.go b/ifd_builder_encode_test.go new file mode 100644 index 0000000..f5517f4 --- /dev/null +++ b/ifd_builder_encode_test.go @@ -0,0 +1,927 @@ +package exif + +import ( + exifcommon "b612.me/exif/common" + "bytes" + "fmt" + "strings" + "testing" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +func Test_IfdByteEncoder_encodeTagToBytes_bytes_thumbnailOffset(t *testing.T) { + defer func() { + if state := recover(); state != nil { + err := log.Wrap(state.(error)) + log.PrintError(err) + t.Fatalf("Test failed.") + } + }() + + ibe := NewIfdByteEncoder() + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + log.PanicIf(err) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + tagId: ThumbnailOffsetTagId, + typeId: exifcommon.TypeLong, + value: NewIfdBuilderTagValueFromBytes([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}), + } + + b := new(bytes.Buffer) + bw := NewByteWriter(b, exifcommon.TestDefaultByteOrder) + + addressableOffset := uint32(0x1234) + ida := newIfdDataAllocator(addressableOffset) + + _, 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)) + } else if bytes.Compare(tagBytes, []byte{ + 0x02, 0x01, + 0x00, 0x04, + 0x00, 0x00, 0x00, 0x01, // unitCount = 1 + 0x00, 0x00, 0x12, 0x34, + }) != 0 { + t.Fatalf("encoded tag-entry not correct") + } +} + +func Test_ByteWriter_writeAsBytes_uint8(t *testing.T) { + b := new(bytes.Buffer) + bw := NewByteWriter(b, exifcommon.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, exifcommon.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, exifcommon.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, exifcommon.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, exifcommon.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, exifcommon.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, exifcommon.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.PrintError(err) + t.Fatalf("Test failed.") + } + }() + + ibe := NewIfdByteEncoder() + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdGpsInfoStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + it, err := ti.Get(ib.IfdIdentity(), uint16(0x0000)) + log.PanicIf(err) + + bt := NewStandardBuilderTag(exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString(), it, exifcommon.TestDefaultByteOrder, []uint8{uint8(0x12)}) + + b := new(bytes.Buffer) + bw := NewByteWriter(b, exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdGpsInfoStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + it, err := ti.Get(ib.IfdIdentity(), uint16(0x0000)) + log.PanicIf(err) + + bt := NewStandardBuilderTag(exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString(), it, exifcommon.TestDefaultByteOrder, []uint8{uint8(0x12), uint8(0x34), uint8(0x56), uint8(0x78)}) + + b := new(bytes.Buffer) + bw := NewByteWriter(b, exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdGpsInfoStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + b := new(bytes.Buffer) + bw := NewByteWriter(b, exifcommon.TestDefaultByteOrder) + + addressableOffset := uint32(0x1234) + ida := newIfdDataAllocator(addressableOffset) + + it, err := ti.Get(ib.IfdIdentity(), uint16(0x0000)) + log.PanicIf(err) + + bt := NewStandardBuilderTag(exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString(), it, exifcommon.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(exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString(), it, exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + b := new(bytes.Buffer) + bw := NewByteWriter(b, exifcommon.TestDefaultByteOrder) + + addressableOffset := uint32(0x1234) + ida := newIfdDataAllocator(addressableOffset) + + childIb := NewIfdBuilder(im, ti, exifcommon.IfdExifStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + tagValue := NewIfdBuilderTagValueFromIfdBuilder(childIb) + bt := NewChildIfdBuilderTag(exifcommon.IfdStandardIfdIdentity.UnindexedString(), exifcommon.IfdExifStandardIfdIdentity.TagId(), 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.PrintError(err) + t.Fatalf("Test failed.") + } + }() + + // 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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + childIb := NewIfdBuilder(im, ti, exifcommon.IfdExifStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + childIbTestTag := &BuilderTag{ + ifdPath: exifcommon.IfdExifStandardIfdIdentity.UnindexedString(), + tagId: 0x8822, + typeId: exifcommon.TypeShort, + value: NewIfdBuilderTagValueFromBytes([]byte{0x12, 0x34}), + } + + childIb.Add(childIbTestTag) + + // Formally compose the tag that refers to it. + + tagValue := NewIfdBuilderTagValueFromIfdBuilder(childIb) + bt := NewChildIfdBuilderTag(exifcommon.IfdStandardIfdIdentity.UnindexedString(), exifcommon.IfdExifStandardIfdIdentity.TagId(), 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, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + b := new(bytes.Buffer) + bw := NewByteWriter(b, exifcommon.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, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder, tagBytes) + log.PanicIf(err) + + if iteV.TagId() != exifcommon.IfdExifStandardIfdIdentity.TagId() { + 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() != exifcommon.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.getValueOffset() != nextIfdOffsetToWrite { + t.Fatalf("IFD's child-IFD offset (as offset) is not correct: (%d) != (%d)", iteV.getValueOffset(), nextIfdOffsetToWrite) + } else if iteV.ChildIfdPath() != exifcommon.IfdExifStandardIfdIdentity.UnindexedString() { + t.Fatalf("IFD first tag IFD-name name not correct: [%s]", iteV.ChildIfdPath()) + } else if iteV.IfdPath() != exifcommon.IfdStandardIfdIdentity.UnindexedString() { + 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, exifcommon.IfdExifStandardIfdIdentity, exifcommon.TestDefaultByteOrder, childIfdBlock, nil) + 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() != exifcommon.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.ChildIfdPath() != "" { + t.Fatalf("Child IFD first tag IFD-name name not empty: [%s]", ite.ChildIfdPath()) + } else if ite.IfdPath() != exifcommon.IfdExifStandardIfdIdentity.UnindexedString() { + 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.PrintError(err) + t.Fatalf("Test failed.") + } + }() + + // 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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + it, err := ib.tagIndex.Get(ib.IfdIdentity(), uint16(0x000b)) + log.PanicIf(err) + + valueString := "testvalue" + bt := NewStandardBuilderTag(exifcommon.IfdStandardIfdIdentity.UnindexedString(), it, exifcommon.TestDefaultByteOrder, valueString) + + b := new(bytes.Buffer) + bw := NewByteWriter(b, exifcommon.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, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder, tagBytes) + 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() != exifcommon.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.ChildIfdPath() != "" { + t.Fatalf("Tag's IFD-name should be empty: [%s]", ite.ChildIfdPath()) + } else if ite.IfdPath() != exifcommon.IfdStandardIfdIdentity.UnindexedString() { + 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", exifcommon.DumpBytesClauseToString(tableAndAllocated)) + } +} + +func Test_IfdByteEncoder_encodeIfdToBytes_fullExif(t *testing.T) { + defer func() { + if state := recover(); state != nil { + err := log.Wrap(state.(error)) + log.PrintError(err) + t.Fatalf("Test failed.") + } + }() + + 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(exifcommon.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.PrintError(err) + t.Fatalf("Test failed.") + } + }() + + 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(exifcommon.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.PrintError(err) + t.Fatalf("Test failed.") + } + }() + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.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, exifcommon.IfdExifStandardIfdIdentity, exifcommon.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, exifcommon.IfdGpsInfoStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + err = childIb2.AddStandardWithName("GPSAltitudeRef", []uint8{0x11, 0x22}) + log.PanicIf(err) + + err = ib.AddChildIb(childIb2) + log.PanicIf(err) + + err = ib.AddStandard(0x013e, []exifcommon.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, exifcommon.IfdStandardIfdIdentity, exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.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", []exifcommon.Rational{{Numerator: 0x11112222, Denominator: 0x33334444}}) + log.PanicIf(err) + + err = ib.AddStandardWithName("ShutterSpeedValue", []exifcommon.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) + + for i, ite := range index.RootIfd.Entries() { + value, err := ite.Value() + log.PanicIf(err) + + fmt.Printf("%d: %s [%v]\n", i, ite, value) + } + + // Output: + // + // 0: IfdTagEntry [asciivalue] + // 1: IfdTagEntry [[17]] + // 2: IfdTagEntry [[8755]] + // 3: IfdTagEntry [[1146447479]] + // 4: IfdTagEntry [[{286335522 858997828}]] + // 5: IfdTagEntry [[{286335522 858997828}]] +} diff --git a/ifd_builder_test.go b/ifd_builder_test.go new file mode 100644 index 0000000..c054690 --- /dev/null +++ b/ifd_builder_test.go @@ -0,0 +1,2331 @@ +package exif + +import ( + "bytes" + "fmt" + "reflect" + "sort" + "strings" + "testing" + "time" + + "b612.me/exif/common" + "b612.me/exif/undefined" + "github.com/dsoprea/go-logging" +) + +func TestIfdBuilder_Add(t *testing.T) { + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x33, + value: NewIfdBuilderTagValueFromBytes([]byte("test string3")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + originalBytes := []byte{0x11, 0x22, 0x33} + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x44, + value: NewIfdBuilderTagValueFromBytes([]byte(originalBytes)), + } + + err = ib.Add(bt) + log.PanicIf(err) + + if ib.ifdIdentity.UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() { + t.Fatalf("IFD name not correct.") + } else if ib.IfdIdentity().TagId() != 0 { + t.Fatalf("IFD tag-ID not correct.") + } else if ib.byteOrder != exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + ib1 := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + ib2 := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + ibChild := NewIfdBuilder(im, ti, exifcommon.IfdExifStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + err = ib.AddChildIb(ibChild) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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.IfdIdentity().TagId() { + t.Fatalf("second tag ID does not match child-IFD tag-ID: (0x%04x) != (0x%04x)", ib.tags[1].tagId, ibChild.IfdIdentity().TagId()) + } 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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, exifData) + log.PanicIf(err) + + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + err = ib.AddTagsFromExisting(index.RootIfd, 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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, exifData) + log.PanicIf(err) + + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + err = ib.AddTagsFromExisting(index.RootIfd, []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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, exifData) + log.PanicIf(err) + + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + err = ib.AddTagsFromExisting(index.RootIfd, 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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x33, + value: NewIfdBuilderTagValueFromBytes([]byte("test string3")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string4")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x33, + value: NewIfdBuilderTagValueFromBytes([]byte("test string3")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string4")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string5")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x33, + value: NewIfdBuilderTagValueFromBytes([]byte("test string3")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x33, + value: NewIfdBuilderTagValueFromBytes([]byte("test string3")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x33, + value: NewIfdBuilderTagValueFromBytes([]byte("test string3")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string3")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string3")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string3")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + bt := &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x11, + value: NewIfdBuilderTagValueFromBytes([]byte("test string")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string2")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.TypeByte, + tagId: 0x22, + value: NewIfdBuilderTagValueFromBytes([]byte("test string3")), + } + + err = ib.Add(bt) + log.PanicIf(err) + + bt = &BuilderTag{ + ifdPath: exifcommon.IfdStandardIfdIdentity.UnindexedString(), + typeId: exifcommon.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_NewIfdBuilderFromExistingChain(t *testing.T) { + defer func() { + if state := recover(); state != nil { + err := log.Wrap(state.(error)) + log.PrintErrorf(err, "Test failure.") + } + }() + + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, rawExif) + log.PanicIf(err) + + ib := NewIfdBuilderFromExistingChain(index.RootIfd) + + actual := ib.DumpToStrings() + + expected := []string{ + "IFD", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "IFD", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "IFDIFD/Exif] FQ-IFD-PATH=[IFD/Exif/Iop] IFD-INDEX=(0) IFD-TAG-ID=(0xa005) TAG=[0xa005]>", + "TAGIFD/Exif] FQ-IFD-PATH=[IFD/Exif/Iop] IFD-TAG-ID=(0xa005) CHILD-IFD=[] TAG-INDEX=(0) TAG=[0x0001]>", + "TAGIFD/Exif] FQ-IFD-PATH=[IFD/Exif/Iop] IFD-TAG-ID=(0xa005) CHILD-IFD=[] TAG-INDEX=(1) TAG=[0x0002]>", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "IFD", + "TAG", + "IFD", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + "TAG", + } + + 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.") + } +} + +func TestIfdBuilder_SetStandardWithName_UpdateGps(t *testing.T) { + defer func() { + if state := recover(); state != nil { + err := log.Wrap(state.(error)) + log.PrintErrorf(err, "Test failure.") + } + }() + + // Check initial value. + + filepath := getTestGpsImageFilepath() + + rawExif, err := SearchFileAndExtractExif(filepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, rawExif) + log.PanicIf(err) + + rootIfd := index.RootIfd + + gpsIfd, err := rootIfd.ChildWithIfdPath(exifcommon.IfdGpsInfoStandardIfdIdentity) + log.PanicIf(err) + + initialGi, err := gpsIfd.GpsInfo() + log.PanicIf(err) + + initialGpsLatitudePhrase := "Degrees" + + if initialGi.Latitude.String() != initialGpsLatitudePhrase { + t.Fatalf("Initial GPS latitude not correct: [%s]", initialGi.Latitude) + } + + // Update the value. + + rootIb := NewIfdBuilderFromExistingChain(rootIfd) + + gpsIb, err := rootIb.ChildWithTagId(exifcommon.IfdGpsInfoStandardIfdIdentity.TagId()) + log.PanicIf(err) + + updatedGi := GpsDegrees{ + Degrees: 11, + Minutes: 22, + Seconds: 33, + } + + raw := updatedGi.Raw() + + err = gpsIb.SetStandardWithName("GPSLatitude", raw) + log.PanicIf(err) + + // Encode to bytes. + + ibe := NewIfdByteEncoder() + + updatedRawExif, err := ibe.EncodeToExif(rootIb) + log.PanicIf(err) + + // Decode from bytes. + + _, updatedIndex, err := Collect(im, ti, updatedRawExif) + log.PanicIf(err) + + updatedRootIfd := updatedIndex.RootIfd + + // Test. + + updatedGpsIfd, err := updatedRootIfd.ChildWithIfdPath(exifcommon.IfdGpsInfoStandardIfdIdentity) + log.PanicIf(err) + + recoveredUpdatedGi, err := updatedGpsIfd.GpsInfo() + log.PanicIf(err) + + updatedGpsLatitudePhrase := "Degrees" + + if recoveredUpdatedGi.Latitude.String() != updatedGpsLatitudePhrase { + t.Fatalf("Updated GPS latitude not set or recovered correctly: [%s]", recoveredUpdatedGi.Latitude) + } +} + +func ExampleIfdBuilder_SetStandardWithName_updateGps() { + // Check initial value. + + filepath := getTestGpsImageFilepath() + + rawExif, err := SearchFileAndExtractExif(filepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, rawExif) + log.PanicIf(err) + + rootIfd := index.RootIfd + + gpsIfd, err := rootIfd.ChildWithIfdPath(exifcommon.IfdGpsInfoStandardIfdIdentity) + log.PanicIf(err) + + initialGi, err := gpsIfd.GpsInfo() + log.PanicIf(err) + + fmt.Printf("Original:\n%s\n\n", initialGi.Latitude.String()) + + // Update the value. + + rootIb := NewIfdBuilderFromExistingChain(rootIfd) + + gpsIb, err := rootIb.ChildWithTagId(exifcommon.IfdGpsInfoStandardIfdIdentity.TagId()) + log.PanicIf(err) + + updatedGi := GpsDegrees{ + Degrees: 11, + Minutes: 22, + Seconds: 33, + } + + raw := updatedGi.Raw() + + err = gpsIb.SetStandardWithName("GPSLatitude", raw) + log.PanicIf(err) + + // Encode to bytes. + + ibe := NewIfdByteEncoder() + + updatedRawExif, err := ibe.EncodeToExif(rootIb) + log.PanicIf(err) + + // Decode from bytes. + + _, updatedIndex, err := Collect(im, ti, updatedRawExif) + log.PanicIf(err) + + updatedRootIfd := updatedIndex.RootIfd + + // Test. + + updatedGpsIfd, err := updatedRootIfd.ChildWithIfdPath(exifcommon.IfdGpsInfoStandardIfdIdentity) + log.PanicIf(err) + + recoveredUpdatedGi, err := updatedGpsIfd.GpsInfo() + log.PanicIf(err) + + fmt.Printf("Updated, written, and re-read:\n%s\n", recoveredUpdatedGi.Latitude.String()) + + // Output: + // Original: + // Degrees + // + // Updated, written, and re-read: + // Degrees +} + +func ExampleIfdBuilder_SetStandardWithName_timestamp() { + // Check initial value. + + filepath := getTestGpsImageFilepath() + + rawExif, err := SearchFileAndExtractExif(filepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, rawExif) + log.PanicIf(err) + + rootIfd := index.RootIfd + + // Update the value. + + rootIb := NewIfdBuilderFromExistingChain(rootIfd) + + exifIb, err := rootIb.ChildWithTagId(exifcommon.IfdExifStandardIfdIdentity.TagId()) + log.PanicIf(err) + + t := time.Date(2020, 06, 7, 1, 30, 0, 0, time.UTC) + + err = exifIb.SetStandardWithName("DateTimeDigitized", t) + log.PanicIf(err) + + // Encode to bytes. + + ibe := NewIfdByteEncoder() + + updatedRawExif, err := ibe.EncodeToExif(rootIb) + log.PanicIf(err) + + // Decode from bytes. + + _, updatedIndex, err := Collect(im, ti, updatedRawExif) + log.PanicIf(err) + + updatedRootIfd := updatedIndex.RootIfd + + // Test. + + updatedExifIfd, err := updatedRootIfd.ChildWithIfdPath(exifcommon.IfdExifStandardIfdIdentity) + log.PanicIf(err) + + results, err := updatedExifIfd.FindTagWithName("DateTimeDigitized") + log.PanicIf(err) + + ite := results[0] + + phrase, err := ite.FormatFirst() + log.PanicIf(err) + + fmt.Printf("%s\n", phrase) + + // Output: + // 2020:06:07 01:30:00 +} + +func TestIfdBuilder_NewIfdBuilderFromExistingChain_RealData(t *testing.T) { + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + // Decode from binary. + + im, err := exifcommon.NewIfdMappingWithStandard() + 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) + + 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.") + } + + originalTagPhrases := make([]string, 0) + for _, ite := range originalTags { + // Adds a lot of noise if/when debugging, and we're already checking the + // thumbnail bytes separately. + if ite.IsThumbnailOffset() == true || ite.IsThumbnailSize() == true { + continue + } + + phrase := ite.String() + + // The value (the offset) of IFDs will almost never be the same after + // reconstruction (by design). + if ite.ChildIfdName() == "" { + valuePhrase, err := ite.FormatFirst() + log.PanicIf(err) + + phrase += " " + valuePhrase + } + + originalTagPhrases = append(originalTagPhrases, phrase) + } + + sort.Strings(originalTagPhrases) + + recoveredTagPhrases := make([]string, 0) + for _, ite := range recoveredTags { + // Adds a lot of noise if/when debugging, and we're already checking the + // thumbnail bytes separately. + if ite.IsThumbnailOffset() == true || ite.IsThumbnailSize() == true { + continue + } + + phrase := ite.String() + + // The value (the offset) of IFDs will almost never be the same after + // reconstruction (by design). + if ite.ChildIfdName() == "" { + valuePhrase, err := ite.FormatFirst() + log.PanicIf(err) + + phrase += " " + valuePhrase + } + + recoveredTagPhrases = append(recoveredTagPhrases, phrase) + } + + sort.Strings(recoveredTagPhrases) + + if reflect.DeepEqual(recoveredTagPhrases, originalTagPhrases) != true { + fmt.Printf("ORIGINAL:\n") + fmt.Printf("\n") + + for _, tag := range originalTagPhrases { + fmt.Printf("%s\n", tag) + } + + fmt.Printf("\n") + + fmt.Printf("RECOVERED:\n") + fmt.Printf("\n") + + for _, tag := range recoveredTagPhrases { + fmt.Printf("%s\n", tag) + } + + fmt.Printf("\n") + + t.Fatalf("Recovered tags do not equal original tags.") + } +} + +// func TestIfdBuilder_NewIfdBuilderFromExistingChain_RealData_WithUpdate(t *testing.T) { +// testImageFilepath := getTestImageFilepath() + +// 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) + +// // Update a tag,. + +// exifBt, err := rootIb.FindTagWithName("ExifTag") +// log.PanicIf(err) + +// ucBt, err := exifBt.value.Ib().FindTagWithName("UserComment") +// log.PanicIf(err) + +// uc := exifundefined.Tag9286UserComment{ +// EncodingType: TagUndefinedType_9286_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() { + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, rawExif) + log.PanicIf(err) + + // This returns the raw bytes that you will be looking for, but there's no + // use for them at this point in the example. + _, err = index.RootIfd.nextIfd.Thumbnail() + log.PanicIf(err) + + // Output: +} + +func ExampleBuilderTag_SetValue() { + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, rawExif) + log.PanicIf(err) + + // Create builder. + + rootIb := NewIfdBuilderFromExistingChain(index.RootIfd) + + // 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 := exifundefined.Tag9286UserComment{ + EncodingType: exifundefined.TagUndefinedType_9286_UserComment_Encoding_ASCII, + EncodingBytes: []byte("TEST COMMENT"), + } + + err = ucBt.SetValue(rootIb.byteOrder, uc) + log.PanicIf(err) + + // Encode. + + ibe := NewIfdByteEncoder() + + // This returns the raw bytes that you will be looking for, but there's no + // use for them at this point in the example. + _, err = ibe.EncodeToExif(rootIb) + log.PanicIf(err) + + // 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() { + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + // Boilerplate. + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + // Load current IFDs. + + _, index, err := Collect(im, ti, rawExif) + log.PanicIf(err) + + ib := NewIfdBuilderFromExistingChain(index.RootIfd) + + // 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 { + valueRaw, err := ite.Value() + log.PanicIf(err) + + stringValue := valueRaw.(string) + fmt.Println(stringValue) + } + + // Output: + // alternative software +} + +func TestIfdBuilder_CreateIfdBuilderWithExistingIfd(t *testing.T) { + ti := NewTagIndex() + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + mi, err := im.GetWithPath(exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString()) + log.PanicIf(err) + + tagId := mi.TagId + + parentIfd := &Ifd{ + ifdIdentity: exifcommon.IfdStandardIfdIdentity, + tagIndex: ti, + } + + ifd := &Ifd{ + ifdIdentity: exifcommon.IfdGpsInfoStandardIfdIdentity, + byteOrder: exifcommon.TestDefaultByteOrder, + offset: 0x123, + parentIfd: parentIfd, + + ifdMapping: im, + tagIndex: ti, + } + + ib := NewIfdBuilderWithExistingIfd(ifd) + + if ib.IfdIdentity().UnindexedString() != ifd.ifdIdentity.UnindexedString() { + t.Fatalf("IFD-name not correct.") + } else if ib.IfdIdentity().TagId() != 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(exifcommon.IfdExifStandardIfdIdentity, uint16(0x8833)) + log.PanicIf(err) + + bt := NewStandardBuilderTag(exifcommon.IfdExifStandardIfdIdentity.UnindexedString(), it, exifcommon.TestDefaultByteOrder, []uint32{uint32(0x1234)}) + + if bt.ifdPath != exifcommon.IfdExifStandardIfdIdentity.UnindexedString() { + 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(exifcommon.IfdExifStandardIfdIdentity, uint16(0x8833)) + log.PanicIf(err) + + bt := NewStandardBuilderTag(exifcommon.IfdExifStandardIfdIdentity.UnindexedString(), it, exifcommon.TestDefaultByteOrder, []uint32{uint32(0x1234), uint32(0x5678)}) + + if bt.ifdPath != exifcommon.IfdExifStandardIfdIdentity.UnindexedString() { + 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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.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 != exifcommon.IfdStandardIfdIdentity.UnindexedString() { + 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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + rootIb := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + rootIb := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + rootIb := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.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, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + rootIb := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder) + + lines := rootIb.DumpToStrings() + expected := []string{ + "IFD", + } + + 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.IfdIdentity().String() != "IFD/Exif" { + t.Fatalf("Returned IB does not have the expected path (IFD/Exif).") + } + + lines = rootIb.DumpToStrings() + expected = []string{ + "IFD", + "TAG", + "IFD", + } + + 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.IfdIdentity().String() != "IFD/Exif/Iop" { + t.Fatalf("Returned IB does not have the expected path (IFD/Exif/Iop).") + } + + lines = rootIb.DumpToStrings() + expected = []string{ + "IFD", + "TAG", + "IFD", + "TAG", + "IFDIFD/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.IfdIdentity().String() != "IFD1" { + t.Fatalf("Returned IB does not have the expected path (IFD1).") + } + + lines = rootIb.DumpToStrings() + expected = []string{ + "IFD", + "TAG", + "IFD", + "TAG", + "IFDIFD/Exif] FQ-IFD-PATH=[IFD/Exif/Iop] IFD-INDEX=(0) IFD-TAG-ID=(0xa005) TAG=[0xa005]>", + "IFD", + } + + 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/ifd_enumerate.go b/ifd_enumerate.go new file mode 100644 index 0000000..60300b6 --- /dev/null +++ b/ifd_enumerate.go @@ -0,0 +1,1672 @@ +package exif + +import ( + "bytes" + "errors" + "fmt" + "io" + "strconv" + "strings" + "time" + + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" + "b612.me/exif/undefined" +) + +var ( + ifdEnumerateLogger = log.NewLogger("exif.ifd_enumerate") +) + +var ( + // ErrNoThumbnail means that no thumbnail was found. + ErrNoThumbnail = errors.New("no thumbnail") + + // ErrNoGpsTags means that no GPS info was found. + ErrNoGpsTags = errors.New("no gps tags") + + // ErrTagTypeNotValid means that the tag-type is not valid. + ErrTagTypeNotValid = errors.New("tag type invalid") + + // ErrOffsetInvalid means that the file offset is not valid. + ErrOffsetInvalid = errors.New("file offset invalid") +) + +var ( + // ValidGpsVersions is the list of recognized EXIF GPS versions/signatures. + ValidGpsVersions = [][4]byte{ + // 2.0.0.0 appears to have a very similar format to 2.2.0.0, so enabling + // it under that assumption. + // + // IFD-PATH=[IFD] ID=(0x8825) NAME=[GPSTag] COUNT=(1) TYPE=[LONG] VALUE=[114] + // IFD-PATH=[IFD/GPSInfo] ID=(0x0000) NAME=[GPSVersionID] COUNT=(4) TYPE=[BYTE] VALUE=[02 00 00 00] + // IFD-PATH=[IFD/GPSInfo] ID=(0x0001) NAME=[GPSLatitudeRef] COUNT=(2) TYPE=[ASCII] VALUE=[S] + // IFD-PATH=[IFD/GPSInfo] ID=(0x0002) NAME=[GPSLatitude] COUNT=(3) TYPE=[RATIONAL] VALUE=[38/1...] + // IFD-PATH=[IFD/GPSInfo] ID=(0x0003) NAME=[GPSLongitudeRef] COUNT=(2) TYPE=[ASCII] VALUE=[E] + // IFD-PATH=[IFD/GPSInfo] ID=(0x0004) NAME=[GPSLongitude] COUNT=(3) TYPE=[RATIONAL] VALUE=[144/1...] + // IFD-PATH=[IFD/GPSInfo] ID=(0x0012) NAME=[GPSMapDatum] COUNT=(7) TYPE=[ASCII] VALUE=[WGS-84] + // + {2, 0, 0, 0}, + + {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}, + } +) + +// byteParser 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 byteParser struct { + byteOrder binary.ByteOrder + rs io.ReadSeeker + ifdOffset uint32 + currentOffset uint32 +} + +// newByteParser returns a new byteParser struct. +// +// initialOffset is for arithmetic-based tracking of where we should be at in +// the stream. +func newByteParser(rs io.ReadSeeker, byteOrder binary.ByteOrder, initialOffset uint32) (bp *byteParser, err error) { + // TODO(dustin): Add test + + bp = &byteParser{ + rs: rs, + byteOrder: byteOrder, + currentOffset: initialOffset, + } + + return bp, nil +} + +// 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 (bp *byteParser) getUint16() (value uint16, raw []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test + + needBytes := 2 + + raw = make([]byte, needBytes) + + _, err = io.ReadFull(bp.rs, raw) + log.PanicIf(err) + + value = bp.byteOrder.Uint16(raw) + + bp.currentOffset += uint32(needBytes) + + 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 (bp *byteParser) getUint32() (value uint32, raw []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test + + needBytes := 4 + + raw = make([]byte, needBytes) + + _, err = io.ReadFull(bp.rs, raw) + log.PanicIf(err) + + value = bp.byteOrder.Uint32(raw) + + bp.currentOffset += uint32(needBytes) + + return value, raw, nil +} + +// CurrentOffset returns the starting offset but the number of bytes that we +// have parsed. This is arithmetic-based tracking, not a seek(0) operation. +func (bp *byteParser) CurrentOffset() uint32 { + return bp.currentOffset +} + +// IfdEnumerate is the main enumeration type. It knows how to parse the IFD +// containers in the EXIF blob. +type IfdEnumerate struct { + ebs ExifBlobSeeker + byteOrder binary.ByteOrder + tagIndex *TagIndex + ifdMapping *exifcommon.IfdMapping + furthestOffset uint32 + + visitedIfdOffsets map[uint32]struct{} +} + +// NewIfdEnumerate returns a new instance of IfdEnumerate. +func NewIfdEnumerate(ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, ebs ExifBlobSeeker, byteOrder binary.ByteOrder) *IfdEnumerate { + return &IfdEnumerate{ + ebs: ebs, + byteOrder: byteOrder, + ifdMapping: ifdMapping, + tagIndex: tagIndex, + + visitedIfdOffsets: make(map[uint32]struct{}), + } +} + +func (ie *IfdEnumerate) getByteParser(ifdOffset uint32) (bp *byteParser, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + initialOffset := ExifAddressableAreaStart + ifdOffset + + rs, err := ie.ebs.GetReadSeeker(int64(initialOffset)) + log.PanicIf(err) + + bp, err = + newByteParser( + rs, + ie.byteOrder, + initialOffset) + + if err != nil { + if err == ErrOffsetInvalid { + return nil, err + } + + log.Panic(err) + } + + return bp, nil +} + +func (ie *IfdEnumerate) parseTag(ii *exifcommon.IfdIdentity, tagPosition int, bp *byteParser) (ite *IfdTagEntry, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + tagId, _, err := bp.getUint16() + log.PanicIf(err) + + tagTypeRaw, _, err := bp.getUint16() + log.PanicIf(err) + + tagType := exifcommon.TagTypePrimitive(tagTypeRaw) + + unitCount, _, err := bp.getUint32() + log.PanicIf(err) + + valueOffset, rawValueOffset, err := bp.getUint32() + log.PanicIf(err) + + // Check whether the embedded type indicator is valid. + + if tagType.IsValid() == false { + // Technically, we have the type on-file in the tags-index, but + // if the type stored alongside the data disagrees with it, + // which it apparently does, all bets are off. + ifdEnumerateLogger.Warningf(nil, + "Tag (0x%04x) in IFD [%s] at position (%d) has invalid type (0x%04x) and will be skipped.", + tagId, ii, tagPosition, int(tagType)) + + ite = &IfdTagEntry{ + tagId: tagId, + tagType: tagType, + } + + return ite, ErrTagTypeNotValid + } + + // Check whether the embedded type is listed among the supported types for + // the registered tag. If not, skip processing the tag. + + it, err := ie.tagIndex.Get(ii, tagId) + if err != nil { + if log.Is(err, ErrTagNotFound) == true { + ifdEnumerateLogger.Warningf(nil, "Tag (0x%04x) is not known and will be skipped.", tagId) + + ite = &IfdTagEntry{ + tagId: tagId, + } + + return ite, ErrTagNotFound + } + + log.Panic(err) + } + + // If we're trying to be as forgiving as possible then use whatever type was + // reported in the format. Otherwise, only accept a type that's expected for + // this tag. + if ie.tagIndex.UniversalSearch() == false && it.DoesSupportType(tagType) == false { + // The type in the stream disagrees with the type that this tag is + // expected to have. This can present issues with how we handle the + // special-case tags (e.g. thumbnails, GPS, etc..) when those tags + // suddenly have data that we no longer manipulate correctly/ + // accurately. + ifdEnumerateLogger.Warningf(nil, + "Tag (0x%04x) in IFD [%s] at position (%d) has unsupported type (0x%02x) and will be skipped.", + tagId, ii, tagPosition, int(tagType)) + + return nil, ErrTagTypeNotValid + } + + // Construct tag struct. + + rs, err := ie.ebs.GetReadSeeker(0) + log.PanicIf(err) + + ite = newIfdTagEntry( + ii, + tagId, + tagPosition, + tagType, + unitCount, + valueOffset, + rawValueOffset, + rs, + ie.byteOrder) + + ifdPath := ii.UnindexedString() + + // 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 { + currentIfdTag := ii.IfdTag() + + childIt := exifcommon.NewIfdTag(¤tIfdTag, tagId, mi.Name) + iiChild := ii.NewChild(childIt, 0) + ite.SetChildIfd(iiChild) + + // 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, exifcommon.ErrChildIfdNotMapped) == false { + log.Panic(err) + } + + return ite, nil +} + +// TagVisitorFn is called for each tag when enumerating through the EXIF. +type TagVisitorFn func(ite *IfdTagEntry) (err error) + +// tagPostParse do some tag-level processing here following the parse of each. +func (ie *IfdEnumerate) tagPostParse(ite *IfdTagEntry, med *MiscellaneousExifData) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test + + ii := ite.IfdIdentity() + + tagId := ite.TagId() + tagType := ite.TagType() + + it, err := ie.tagIndex.Get(ii, tagId) + if err == nil { + ite.setTagName(it.Name) + } else { + if err != ErrTagNotFound { + log.Panic(err) + } + + // This is an unknown tag. + + originalBt := exifcommon.BasicTag{ + FqIfdPath: ii.String(), + IfdPath: ii.UnindexedString(), + TagId: tagId, + } + + if med != nil { + med.unknownTags[originalBt] = exifcommon.BasicTag{} + } + + utilityLogger.Debugf(nil, + "Tag (0x%04x) is not valid for IFD [%s]. Attempting secondary "+ + "lookup.", tagId, ii.String()) + + // This will overwrite the existing `it` and `err`. Since `FindFirst()` + // might generate different Errors than `Get()`, the log message above + // is import to try and mitigate confusion in that case. + it, err = ie.tagIndex.FindFirst(tagId, tagType, nil) + if err != nil { + if err != ErrTagNotFound { + log.Panic(err) + } + + // This is supposed to be a convenience function and if we were + // to keep the name empty or set it to some placeholder, it + // might be mismanaged by the package that is calling us. If + // they want to specifically manage these types of tags, they + // can use more advanced functionality to specifically -handle + // unknown tags. + utilityLogger.Warningf(nil, + "Tag with ID (0x%04x) in IFD [%s] is not recognized and "+ + "will be ignored.", tagId, ii.String()) + + return ErrTagNotFound + } + + ite.setTagName(it.Name) + + utilityLogger.Warningf(nil, + "Tag with ID (0x%04x) is not valid for IFD [%s], but it *is* "+ + "valid as tag [%s] under IFD [%s] and has the same type "+ + "[%s], so we will use that. This EXIF blob was probably "+ + "written by a buggy implementation.", + tagId, ii.UnindexedString(), it.Name, it.IfdPath, + tagType) + + if med != nil { + med.unknownTags[originalBt] = exifcommon.BasicTag{ + IfdPath: it.IfdPath, + TagId: tagId, + } + } + } + + // This is a known tag (from the standard, unless the user did + // something different). + + // Skip any tags that have a type that doesn't match the type in the + // index (which is loaded with the standard and accept tag + // information unless configured otherwise). + // + // We've run into multiple instances of the same tag, where a) no + // tag should ever be repeated, and b) all but one had an incorrect + // type and caused parsing/conversion woes. So, this is a quick fix + // for those scenarios. + if ie.tagIndex.UniversalSearch() == false && it.DoesSupportType(tagType) == false { + ifdEnumerateLogger.Warningf(nil, + "Skipping tag [%s] (0x%04x) [%s] with an unexpected type: %v ∉ %v", + ii.UnindexedString(), tagId, it.Name, + tagType, it.SupportedTypes) + + return ErrTagNotFound + } + + return nil +} + +// parseIfd decodes the IFD block that we're currently sitting on the first +// byte of. +func (ie *IfdEnumerate) parseIfd(ii *exifcommon.IfdIdentity, bp *byteParser, visitor TagVisitorFn, doDescend bool, med *MiscellaneousExifData) (nextIfdOffset uint32, entries []*IfdTagEntry, thumbnailData []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + tagCount, _, err := bp.getUint16() + log.PanicIf(err) + + ifdEnumerateLogger.Debugf(nil, "IFD [%s] tag-count: (%d)", ii.String(), tagCount) + + entries = make([]*IfdTagEntry, 0) + + var enumeratorThumbnailOffset *IfdTagEntry + var enumeratorThumbnailSize *IfdTagEntry + + for i := 0; i < int(tagCount); i++ { + ite, err := ie.parseTag(ii, i, bp) + if err != nil { + if log.Is(err, ErrTagNotFound) == true || log.Is(err, ErrTagTypeNotValid) == true { + // These tags should've been fully logged in parseTag(). The + // ITE returned is nil so we can't print anything about them, now. + continue + } + + log.Panic(err) + } + + err = ie.tagPostParse(ite, med) + if err == nil { + if err == ErrTagNotFound { + continue + } + + log.PanicIf(err) + } + + tagId := ite.TagId() + + if visitor != nil { + err := visitor(ite) + log.PanicIf(err) + } + + if ite.IsThumbnailOffset() == true { + ifdEnumerateLogger.Debugf(nil, "Skipping the thumbnail offset tag (0x%04x). Use accessors to get it or set it.", tagId) + + enumeratorThumbnailOffset = ite + entries = append(entries, ite) + + continue + } else if ite.IsThumbnailSize() == true { + ifdEnumerateLogger.Debugf(nil, "Skipping the thumbnail size tag (0x%04x). Use accessors to get it or set it.", tagId) + + enumeratorThumbnailSize = ite + entries = append(entries, ite) + + continue + } + + if ite.TagType() != exifcommon.TypeUndefined { + // If this tag's value is an offset, bump our max-offset value to + // what that offset is plus however large that value is. + + vc := ite.getValueContext() + + farOffset, err := vc.GetFarOffset() + if err == nil { + candidateOffset := farOffset + uint32(vc.SizeInBytes()) + if candidateOffset > ie.furthestOffset { + ie.furthestOffset = candidateOffset + } + } else if err != exifcommon.ErrNotFarValue { + 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 ite.ChildIfdPath() != "" { + if doDescend == true { + ifdEnumerateLogger.Debugf(nil, "Descending from IFD [%s] to IFD [%s].", ii, ite.ChildIfdPath()) + + currentIfdTag := ii.IfdTag() + + childIfdTag := + exifcommon.NewIfdTag( + ¤tIfdTag, + ite.TagId(), + ite.ChildIfdName()) + + iiChild := ii.NewChild(childIfdTag, 0) + + err := ie.scan(iiChild, ite.getValueOffset(), visitor, med) + log.PanicIf(err) + + ifdEnumerateLogger.Debugf(nil, "Ascending from IFD [%s] to IFD [%s].", ite.ChildIfdPath(), ii) + } + } + + entries = append(entries, ite) + } + + if enumeratorThumbnailOffset != nil && enumeratorThumbnailSize != nil { + thumbnailData, err = ie.parseThumbnail(enumeratorThumbnailOffset, enumeratorThumbnailSize) + if err != nil { + ifdEnumerateLogger.Errorf( + nil, err, + "We tried to bump our furthest-offset counter but there was an issue first seeking past the thumbnail.") + } else { + // In this case, the value is always an offset. + offset := enumeratorThumbnailOffset.getValueOffset() + + // This this case, the value is always a length. + length := enumeratorThumbnailSize.getValueOffset() + + ifdEnumerateLogger.Debugf(nil, "Found thumbnail in IFD [%s]. Its offset is (%d) and is (%d) bytes.", ii, offset, length) + + furthestOffset := offset + length + + if furthestOffset > ie.furthestOffset { + ie.furthestOffset = furthestOffset + } + } + } + + nextIfdOffset, _, err = bp.getUint32() + log.PanicIf(err) + + _, alreadyVisited := ie.visitedIfdOffsets[nextIfdOffset] + + if alreadyVisited == true { + ifdEnumerateLogger.Warningf(nil, "IFD at offset (0x%08x) has been linked-to more than once. There might be a cycle in the IFD chain. Not reparsing.", nextIfdOffset) + nextIfdOffset = 0 + } + + if nextIfdOffset != 0 { + ie.visitedIfdOffsets[nextIfdOffset] = struct{}{} + ifdEnumerateLogger.Debugf(nil, "[%s] Next IFD at offset: (0x%08x)", ii.String(), nextIfdOffset) + } else { + ifdEnumerateLogger.Debugf(nil, "[%s] IFD chain has terminated.", ii.String()) + } + + 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)) + } + }() + + vRaw, err := lengthIte.Value() + 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.updateTagType(exifcommon.TypeByte) + offsetIte.updateUnitCount(length) + + thumbnailData, err = offsetIte.GetRawBytes() + log.PanicIf(err) + + return thumbnailData, nil +} + +// scan parses and enumerates the different IFD blocks and invokes a visitor +// callback for each tag. No information is kept or returned. +func (ie *IfdEnumerate) scan(iiGeneral *exifcommon.IfdIdentity, ifdOffset uint32, visitor TagVisitorFn, med *MiscellaneousExifData) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test + + for ifdIndex := 0; ; ifdIndex++ { + iiSibling := iiGeneral.NewSibling(ifdIndex) + + ifdEnumerateLogger.Debugf(nil, "Parsing IFD [%s] at offset (0x%04x) (scan).", iiSibling.String(), ifdOffset) + + bp, err := ie.getByteParser(ifdOffset) + if err != nil { + if err == ErrOffsetInvalid { + ifdEnumerateLogger.Errorf(nil, nil, "IFD [%s] at offset (0x%04x) is unreachable. Terminating scan.", iiSibling.String(), ifdOffset) + break + } + + log.Panic(err) + } + + nextIfdOffset, _, _, err := ie.parseIfd(iiSibling, bp, visitor, true, med) + log.PanicIf(err) + + currentOffset := bp.CurrentOffset() + if currentOffset > ie.furthestOffset { + ie.furthestOffset = currentOffset + } + + if nextIfdOffset == 0 { + break + } + + ifdOffset = nextIfdOffset + } + + return nil +} + +// MiscellaneousExifData is reports additional data collected during the parse. +type MiscellaneousExifData struct { + // UnknownTags contains all tags that were invalid for their containing + // IFDs. The values represent alternative IFDs that were correctly matched + // to those tags and used instead. + unknownTags map[exifcommon.BasicTag]exifcommon.BasicTag +} + +// UnknownTags returns the unknown tags encountered during the scan. +func (med *MiscellaneousExifData) UnknownTags() map[exifcommon.BasicTag]exifcommon.BasicTag { + return med.unknownTags +} + +// ScanOptions tweaks parser behavior/choices. +type ScanOptions struct { + // NOTE(dustin): Reserved for future usage. +} + +// Scan enumerates the different EXIF blocks (called IFDs). `rootIfdName` will +// be "IFD" in the TIFF standard. +func (ie *IfdEnumerate) Scan(iiRoot *exifcommon.IfdIdentity, ifdOffset uint32, visitor TagVisitorFn, so *ScanOptions) (med *MiscellaneousExifData, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test + + med = &MiscellaneousExifData{ + unknownTags: make(map[exifcommon.BasicTag]exifcommon.BasicTag), + } + + err = ie.scan(iiRoot, ifdOffset, visitor, med) + log.PanicIf(err) + + ifdEnumerateLogger.Debugf(nil, "Scan: It looks like the furthest offset that contained EXIF data in the EXIF blob was (%d) (Scan).", ie.FurthestOffset()) + + return med, nil +} + +// Ifd represents a single, parsed IFD. +type Ifd struct { + ifdIdentity *exifcommon.IfdIdentity + + ifdMapping *exifcommon.IfdMapping + tagIndex *TagIndex + + offset uint32 + byteOrder binary.ByteOrder + 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 + + entries []*IfdTagEntry + entriesByTagId map[uint16][]*IfdTagEntry + + children []*Ifd + childIfdIndex map[string]*Ifd + + thumbnailData []byte + + nextIfdOffset uint32 + nextIfd *Ifd +} + +// IfdIdentity returns IFD identity that this struct represents. +func (ifd *Ifd) IfdIdentity() *exifcommon.IfdIdentity { + return ifd.ifdIdentity +} + +// Entries returns a flat list of all tags for this IFD. +func (ifd *Ifd) Entries() []*IfdTagEntry { + + // TODO(dustin): Add test + + return ifd.entries +} + +// EntriesByTagId returns a map of all tags for this IFD. +func (ifd *Ifd) EntriesByTagId() map[uint16][]*IfdTagEntry { + + // TODO(dustin): Add test + + return ifd.entriesByTagId +} + +// Children returns a flat list of all child IFDs of this IFD. +func (ifd *Ifd) Children() []*Ifd { + + // TODO(dustin): Add test + + return ifd.children +} + +// ChildWithIfdPath returns a map of all child IFDs of this IFD. +func (ifd *Ifd) ChildIfdIndex() map[string]*Ifd { + + // TODO(dustin): Add test + + return ifd.childIfdIndex +} + +// ParentTagIndex returns the position of this IFD's tag in its parent IFD (*if* +// there is a parent). +func (ifd *Ifd) ParentTagIndex() int { + + // TODO(dustin): Add test + + return ifd.parentTagIndex +} + +// Offset returns the offset of the IFD in the stream. +func (ifd *Ifd) Offset() uint32 { + + // TODO(dustin): Add test + + return ifd.offset +} + +// Offset returns the offset of the IFD in the stream. +func (ifd *Ifd) ByteOrder() binary.ByteOrder { + + // TODO(dustin): Add test + + return ifd.byteOrder +} + +// NextIfd returns the Ifd struct for the next IFD in the chain. +func (ifd *Ifd) NextIfd() *Ifd { + + // TODO(dustin): Add test + + return ifd.nextIfd +} + +// ChildWithIfdPath returns an `Ifd` struct for the given child of the current +// IFD. +func (ifd *Ifd) ChildWithIfdPath(iiChild *exifcommon.IfdIdentity) (childIfd *Ifd, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): This is a bridge while we're introducing the IFD type-system. We should be able to use the (IfdIdentity).Equals() method for this. + ifdPath := iiChild.UnindexedString() + + for _, childIfd := range ifd.children { + if childIfd.ifdIdentity.UnindexedString() == ifdPath { + return childIfd, nil + } + } + + log.Panic(ErrTagNotFound) + return nil, 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.ifdIdentity, tagName) + if log.Is(err, ErrTagNotFound) == true { + log.Panic(ErrTagNotKnown) + } 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 +} + +// String returns a description string. +func (ifd *Ifd) String() string { + parentOffset := uint32(0) + if ifd.parentIfd != nil { + parentOffset = ifd.parentIfd.offset + } + + return fmt.Sprintf("Ifd", ifd.id, ifd.ifdIdentity.UnindexedString(), ifd.ifdIdentity.Index(), len(ifd.entries), ifd.offset, len(ifd.children), parentOffset, ifd.nextIfdOffset) +} + +// Thumbnail returns the raw thumbnail bytes. This is typically directly +// readable by any standard image viewer. +func (ifd *Ifd) Thumbnail() (data []byte, err error) { + + if ifd.thumbnailData == nil { + return nil, ErrNoThumbnail + } + + return ifd.thumbnailData, nil +} + +// dumpTags recursively builds a list of tags from an IFD. +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 _, ite := range ifd.entries { + tags = append(tags, ite) + + childIfdPath := ite.ChildIfdPath() + if childIfdPath != "" { + ifdsFoundCount++ + + childIfd, found := ifd.childIfdIndex[childIfdPath] + if found != true { + log.Panicf("alien child IFD referenced by a tag: [%s]", 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 _, ite := range ifd.entries { + if ite.ChildIfdPath() != "" { + fmt.Printf("%s - TAG: %s\n", indent, ite) + } else { + // This will just add noise to the output (byte-tags are fully + // dumped). + if ite.IsThumbnailOffset() == true || ite.IsThumbnailSize() == true { + continue + } + + it, err := ifd.tagIndex.Get(ifd.ifdIdentity, ite.TagId()) + + tagName := "" + if err == nil { + tagName = it.Name + } + + var valuePhrase string + if populateValues == true { + var err error + + valuePhrase, err = ite.Format() + if err != nil { + if log.Is(err, exifcommon.ErrUnhandledUndefinedTypedTag) == true { + ifdEnumerateLogger.Warningf(nil, "Skipping non-standard undefined tag: [%s] (%04x)", ifd.ifdIdentity.UnindexedString(), ite.TagId()) + continue + } else if err == exifundefined.ErrUnparseableValue { + ifdEnumerateLogger.Warningf(nil, "Skipping unparseable undefined tag: [%s] (%04x) [%s]", ifd.ifdIdentity.UnindexedString(), ite.TagId(), it.Name) + continue + } + + log.Panic(err) + } + } else { + valuePhrase = "!UNRESOLVED" + } + + fmt.Printf("%s - TAG: %s NAME=[%s] VALUE=[%v]\n", indent, ite, tagName, valuePhrase) + } + + childIfdPath := ite.ChildIfdPath() + if childIfdPath != "" { + ifdsFoundCount++ + + childIfd, found := ifd.childIfdIndex[childIfdPath] + if found != true { + log.Panicf("alien child IFD referenced by a tag: [%s]", 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 _, ite := range ifd.entries { + childIfdPath := ite.ChildIfdPath() + if childIfdPath != "" { + ifdsFoundCount++ + + childIfd, found := ifd.childIfdIndex[childIfdPath] + if found != true { + log.Panicf("alien child IFD referenced by a tag: [%s]", 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.ifdIdentity.UnindexedString(), ifd.ifdIdentity.UnindexedString(), ifd.ifdIdentity.Index()) + } else { + ifdPhrase = fmt.Sprintf("[ROOT]->[%s]:(%d)", ifd.ifdIdentity.UnindexedString(), ifd.ifdIdentity.Index()) + } + + startBlurb := fmt.Sprintf("%s> IFD %s TOP", indent, ifdPhrase) + tagsDump = append(tagsDump, startBlurb) + + ifdsFoundCount := 0 + for _, ite := range ifd.entries { + tagsDump = append(tagsDump, fmt.Sprintf("%s - (0x%04x)", indent, ite.TagId())) + + childIfdPath := ite.ChildIfdPath() + if childIfdPath != "" { + ifdsFoundCount++ + + childIfd, found := ifd.childIfdIndex[childIfdPath] + if found != true { + log.Panicf("alien child IFD referenced by a tag: [%s]", 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.ifdIdentity.UnindexedString(), ifd.nextIfd.ifdIdentity.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)) + } + }() + + gi = new(GpsInfo) + + if ifd.ifdIdentity.Equals(exifcommon.IfdGpsInfoStandardIfdIdentity) == false { + log.Panicf("GPS can only be read on GPS IFD: [%s]", ifd.ifdIdentity.UnindexedString()) + } + + if tags, found := ifd.entriesByTagId[TagGpsVersionId]; 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.", TagGpsVersionId) + } else { + versionBytes, err := tags[0].GetRawBytes() + log.PanicIf(err) + + hit := false + for _, acceptedGpsVersion := range ValidGpsVersions { + if bytes.Compare(versionBytes, acceptedGpsVersion[:]) == 0 { + hit = true + break + } + } + + if hit != true { + ifdEnumerateLogger.Warningf(nil, "GPS version not supported: %v", versionBytes) + log.Panic(ErrNoGpsTags) + } + } + + tags, found := ifd.entriesByTagId[TagLatitudeId] + if found == false { + ifdEnumerateLogger.Warningf(nil, "latitude not found") + log.Panic(ErrNoGpsTags) + } + + latitudeValue, err := tags[0].Value() + 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 := tags[0].Value() + log.PanicIf(err) + + tags, found = ifd.entriesByTagId[TagLongitudeId] + if found == false { + ifdEnumerateLogger.Warningf(nil, "longitude not found") + log.Panic(ErrNoGpsTags) + } + + longitudeValue, err := tags[0].Value() + 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 := tags[0].Value() + log.PanicIf(err) + + // Parse location. + + latitudeRaw := latitudeValue.([]exifcommon.Rational) + + gi.Latitude, err = NewGpsDegreesFromRationals(latitudeRefValue.(string), latitudeRaw) + log.PanicIf(err) + + longitudeRaw := longitudeValue.([]exifcommon.Rational) + + gi.Longitude, err = NewGpsDegreesFromRationals(longitudeRefValue.(string), longitudeRaw) + log.PanicIf(err) + + // Parse altitude. + + altitudeTags, foundAltitude := ifd.entriesByTagId[TagAltitudeId] + altitudeRefTags, foundAltitudeRef := ifd.entriesByTagId[TagAltitudeRefId] + + if foundAltitude == true && foundAltitudeRef == true { + altitudePhrase, err := altitudeTags[0].Format() + log.PanicIf(err) + + ifdEnumerateLogger.Debugf(nil, "Altitude is [%s].", altitudePhrase) + + altitudeValue, err := altitudeTags[0].Value() + log.PanicIf(err) + + altitudeRefPhrase, err := altitudeRefTags[0].Format() + log.PanicIf(err) + + ifdEnumerateLogger.Debugf(nil, "Altitude-reference is [%s].", altitudeRefPhrase) + + altitudeRefValue, err := altitudeRefTags[0].Value() + log.PanicIf(err) + + altitudeRaw := altitudeValue.([]exifcommon.Rational) + if altitudeRaw[0].Denominator > 0 { + altitude := int(altitudeRaw[0].Numerator / altitudeRaw[0].Denominator) + + if altitudeRefValue.([]byte)[0] == 1 { + altitude *= -1 + } + + gi.Altitude = altitude + } + } + + // Parse timestamp from separate date and time tags. + + timestampTags, foundTimestamp := ifd.entriesByTagId[TagTimestampId] + datestampTags, foundDatestamp := ifd.entriesByTagId[TagDatestampId] + + if foundTimestamp == true && foundDatestamp == true { + datestampValue, err := datestampTags[0].Value() + log.PanicIf(err) + + datePhrase := datestampValue.(string) + ifdEnumerateLogger.Debugf(nil, "Date tag value is [%s].", datePhrase) + + // Normalize the separators. + datePhrase = strings.ReplaceAll(datePhrase, "-", ":") + + dateParts := strings.Split(datePhrase, ":") + + 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 := timestampTags[0].Value() + log.PanicIf(err) + + timePhrase, err := timestampTags[0].Format() + log.PanicIf(err) + + ifdEnumerateLogger.Debugf(nil, "Time tag value is [%s].", timePhrase) + + timestampRaw := timestampValue.([]exifcommon.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 +} + +// ParsedTagVisitor is a callback used if wanting to visit through all tags and +// child IFDs from the current IFD and going down. +type ParsedTagVisitor func(*Ifd, *IfdTagEntry) error + +// EnumerateTagsRecursively calls the given visitor function for every tag and +// IFD in the current IFD, recursively. +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 { + childIfdPath := ite.ChildIfdPath() + if childIfdPath != "" { + childIfd := ifd.childIfdIndex[childIfdPath] + + err := childIfd.EnumerateTagsRecursively(visitor) + log.PanicIf(err) + } else { + err := visitor(ifd, ite) + log.PanicIf(err) + } + } + } + + return nil +} + +// QueuedIfd is one IFD that has been identified but yet to be processed. +type QueuedIfd struct { + IfdIdentity *exifcommon.IfdIdentity + + 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 +} + +// IfdIndex collects a bunch of IFD and tag information stored in several +// different ways in order to provide convenient lookups. +type IfdIndex struct { + RootIfd *Ifd + Ifds []*Ifd + Tree map[int]*Ifd + Lookup map[string]*Ifd +} + +// Collect enumerates the different EXIF blocks (called IFDs) and builds out an +// index struct for referencing all of the parsed data. +func (ie *IfdEnumerate) Collect(rootIfdOffset uint32) (index IfdIndex, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add MiscellaneousExifData to IfdIndex + + tree := make(map[int]*Ifd) + ifds := make([]*Ifd, 0) + lookup := make(map[string]*Ifd) + + queue := []QueuedIfd{ + { + IfdIdentity: exifcommon.IfdStandardIfdIdentity, + Offset: rootIfdOffset, + }, + } + + edges := make(map[uint32]*Ifd) + + for { + if len(queue) == 0 { + break + } + + qi := queue[0] + ii := qi.IfdIdentity + + offset := qi.Offset + parentIfd := qi.Parent + + queue = queue[1:] + + ifdEnumerateLogger.Debugf(nil, "Parsing IFD [%s] (%d) at offset (0x%04x) (Collect).", ii.String(), ii.Index(), offset) + + bp, err := ie.getByteParser(offset) + if err != nil { + if err == ErrOffsetInvalid { + return index, err + } + + log.Panic(err) + } + + // TODO(dustin): We don't need to pass the index in as a separate argument. Get from the II. + + nextIfdOffset, entries, thumbnailData, err := ie.parseIfd(ii, bp, nil, false, nil) + log.PanicIf(err) + + currentOffset := bp.CurrentOffset() + if currentOffset > ie.furthestOffset { + ie.furthestOffset = currentOffset + } + + id := len(ifds) + + entriesByTagId := make(map[uint16][]*IfdTagEntry) + for _, ite := range entries { + tagId := ite.TagId() + + tags, found := entriesByTagId[tagId] + if found == false { + tags = make([]*IfdTagEntry, 0) + } + + entriesByTagId[tagId] = append(tags, ite) + } + + ifd := &Ifd{ + ifdIdentity: ii, + + byteOrder: ie.byteOrder, + + id: id, + + parentIfd: parentIfd, + parentTagIndex: qi.ParentTagIndex, + + 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. + lookup[ii.String()] = ifd + + // 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, ite := range entries { + if ite.ChildIfdPath() == "" { + continue + } + + tagId := ite.TagId() + childIfdName := ite.ChildIfdName() + + currentIfdTag := ii.IfdTag() + + childIfdTag := + exifcommon.NewIfdTag( + ¤tIfdTag, + tagId, + childIfdName) + + iiChild := ii.NewChild(childIfdTag, 0) + + qi := QueuedIfd{ + IfdIdentity: iiChild, + + Offset: ite.getValueOffset(), + Parent: ifd, + ParentTagIndex: i, + } + + queue = append(queue, qi) + } + + // If there's another IFD in the chain. + if nextIfdOffset != 0 { + iiSibling := ii.NewSibling(ii.Index() + 1) + + // Allow the next link to know what the previous link was. + edges[nextIfdOffset] = ifd + + qi := QueuedIfd{ + IfdIdentity: iiSibling, + 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) + + ifdEnumerateLogger.Debugf(nil, "Collect: It looks like the furthest offset that contained EXIF data in the EXIF blob was (%d).", ie.FurthestOffset()) + + 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.ifdIdentity.UnindexedString()] = childIfd + } + + ifd.childIfdIndex = childIfdIndex + + for _, childIfd := range ifd.children { + err := ie.setChildrenIndex(childIfd) + log.PanicIf(err) + } + + return nil +} + +// FurthestOffset returns the furthest offset visited in the EXIF blob. This +// *does not* account for the locations of any undefined tags since we always +// evaluate the furthest offset, whether or not the user wants to know it. +// +// We are not willing to incur the cost of actually parsing those tags just to +// know their length when there are still undefined tags that are out there +// that we still won't have any idea how to parse, thus making this an +// approximation regardless of how clever we get. +func (ie *IfdEnumerate) FurthestOffset() uint32 { + + // TODO(dustin): Add test + + return ie.furthestOffset +} + +// parseOneIfd is a hack to use an IE to parse a raw IFD block. Can be used for +// testing. The fqIfdPath ("fully-qualified IFD path") will be less qualified +// in that the numeric index will always be zero (the zeroth child) rather than +// the proper number (if its actually a sibling to the first child, for +// instance). +func parseOneIfd(ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, ii *exifcommon.IfdIdentity, byteOrder binary.ByteOrder, ifdBlock []byte, visitor TagVisitorFn) (nextIfdOffset uint32, entries []*IfdTagEntry, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test + + ebs := NewExifReadSeekerWithBytes(ifdBlock) + + rs, err := ebs.GetReadSeeker(0) + log.PanicIf(err) + + bp, err := newByteParser(rs, byteOrder, 0) + if err != nil { + if err == ErrOffsetInvalid { + return 0, nil, err + } + + log.Panic(err) + } + + dummyEbs := NewExifReadSeekerWithBytes([]byte{}) + ie := NewIfdEnumerate(ifdMapping, tagIndex, dummyEbs, byteOrder) + + nextIfdOffset, entries, _, err = ie.parseIfd(ii, bp, visitor, true, nil) + log.PanicIf(err) + + return nextIfdOffset, entries, nil +} + +// parseOneTag is a hack to use an IE to parse a raw tag block. +func parseOneTag(ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, ii *exifcommon.IfdIdentity, byteOrder binary.ByteOrder, tagBlock []byte) (ite *IfdTagEntry, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test + + ebs := NewExifReadSeekerWithBytes(tagBlock) + + rs, err := ebs.GetReadSeeker(0) + log.PanicIf(err) + + bp, err := newByteParser(rs, byteOrder, 0) + if err != nil { + if err == ErrOffsetInvalid { + return nil, err + } + + log.Panic(err) + } + + dummyEbs := NewExifReadSeekerWithBytes([]byte{}) + ie := NewIfdEnumerate(ifdMapping, tagIndex, dummyEbs, byteOrder) + + ite, err = ie.parseTag(ii, 0, bp) + log.PanicIf(err) + + err = ie.tagPostParse(ite, nil) + if err != nil { + if err == ErrTagNotFound { + return nil, err + } + + log.Panic(err) + } + + return ite, nil +} + +// FindIfdFromRootIfd returns the given `Ifd` given the root-IFD and path of the +// desired IFD. +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 != exifcommon.IfdStandardIfdIdentity.Name() { + log.Panicf("First IFD path item must be [%s].", exifcommon.IfdStandardIfdIdentity.Name()) + } + + 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 _, itii := range lineage { + var hit *Ifd + for _, childIfd := range thisIfd.children { + if childIfd.ifdIdentity.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", thisIfd.ifdIdentity.UnindexedString(), itii.Index) + } + + thisIfd = thisIfd.nextIfd + } + } + + return thisIfd, nil +} diff --git a/ifd_enumerate_test.go b/ifd_enumerate_test.go new file mode 100644 index 0000000..9327fe0 --- /dev/null +++ b/ifd_enumerate_test.go @@ -0,0 +1,559 @@ +package exif + +import ( + "bytes" + "fmt" + "path" + "reflect" + "testing" + + "io/ioutil" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +func TestIfdTagEntry_RawBytes_RealData(t *testing.T) { + defer func() { + if state := recover(); state != nil { + err := log.Wrap(state.(error)) + log.PrintErrorf(err, "Test failure.") + } + }() + + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, 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.") + } + + decodedBytes, err := ite.GetRawBytes() + 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) { + testImageFilepath := getTestImageFilepath() + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, rawExif) + log.PanicIf(err) + + ifd := index.RootIfd + results, err := ifd.FindTagWithId(0x011b) + log.PanicIf(err) + + 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) { + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + 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) { + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, rawExif) + log.PanicIf(err) + + ifd := index.RootIfd + + results, err := ifd.FindTagWithName("YResolution") + log.PanicIf(err) + + 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) { + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + 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) { + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + 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, ErrTagNotKnown) == false { + log.Panic(err) + } +} + +func TestIfd_Thumbnail(t *testing.T) { + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + 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) + + assetsPath := exifcommon.GetTestAssetsPath() + 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 := getTestGpsImageFilepath() + + rawExif, err := SearchFileAndExtractExif(filepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, rawExif) + log.PanicIf(err) + + ifd, err := index.RootIfd.ChildWithIfdPath(exifcommon.IfdGpsInfoStandardIfdIdentity) + 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_GpsInfo__2_0_0_0(t *testing.T) { + defer func() { + if state := recover(); state != nil { + err := log.Wrap(state.(error)) + log.PrintErrorf(err, "Test failure.") + } + }() + + assetsPath := exifcommon.GetTestAssetsPath() + filepath := path.Join(assetsPath, "gps-2000-scaled.jpg") + + rawExif, err := SearchFileAndExtractExif(filepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, rawExif) + log.PanicIf(err) + + ifd, err := index.RootIfd.ChildWithIfdPath(exifcommon.IfdGpsInfoStandardIfdIdentity) + log.PanicIf(err) + + gi, err := ifd.GpsInfo() + log.PanicIf(err) + + expectedLatitude := GpsDegrees{ + Orientation: 'S', + Degrees: 38.0, + Minutes: 24.311687, + Seconds: 0.0, + } + + expectedLongitude := GpsDegrees{ + Orientation: 'E', + Degrees: 144.0, + Minutes: 11.33748, + Seconds: 0.0, + } + + if GpsDegreesEquals(gi.Latitude, expectedLatitude) != true { + t.Fatalf("Latitude not correct: %v", gi.Latitude) + } else if GpsDegreesEquals(gi.Longitude, expectedLongitude) != true { + t.Fatalf("Longitude not correct: %v", gi.Longitude) + } else if gi.Altitude != 0 { + t.Fatalf("Altitude not correct: (%d)", gi.Altitude) + } else if gi.Timestamp.Unix() != -62135596800 { + t.Fatalf("Timestamp not correct: (%d)", gi.Timestamp.Unix()) + } +} + +func TestIfd_EnumerateTagsRecursively(t *testing.T) { + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + 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.ifdIdentity.UnindexedString(), + int(ite.TagId()), + } + + collected = append(collected, item) + + return nil + } + + err = index.RootIfd.EnumerateTagsRecursively(cb) + log.PanicIf(err) + + expected := [][2]interface{}{ + {"IFD", 0x010f}, + {"IFD", 0x0110}, + {"IFD", 0x0112}, + {"IFD", 0x011a}, + {"IFD", 0x011b}, + {"IFD", 0x0128}, + {"IFD", 0x0132}, + {"IFD", 0x013b}, + {"IFD", 0x0213}, + {"IFD", 0x8298}, + {"IFD/Exif", 0x829a}, + {"IFD/Exif", 0x829d}, + {"IFD/Exif", 0x8822}, + {"IFD/Exif", 0x8827}, + {"IFD/Exif", 0x8830}, + {"IFD/Exif", 0x8832}, + {"IFD/Exif", 0x9000}, + {"IFD/Exif", 0x9003}, + {"IFD/Exif", 0x9004}, + {"IFD/Exif", 0x9101}, + {"IFD/Exif", 0x9201}, + {"IFD/Exif", 0x9202}, + {"IFD/Exif", 0x9204}, + {"IFD/Exif", 0x9207}, + {"IFD/Exif", 0x9209}, + {"IFD/Exif", 0x920a}, + {"IFD/Exif", 0x927c}, + {"IFD/Exif", 0x9286}, + {"IFD/Exif", 0x9290}, + {"IFD/Exif", 0x9291}, + {"IFD/Exif", 0x9292}, + {"IFD/Exif", 0xa000}, + {"IFD/Exif", 0xa001}, + {"IFD/Exif", 0xa002}, + {"IFD/Exif", 0xa003}, + {"IFD/Exif/Iop", 0x0001}, + {"IFD/Exif/Iop", 0x0002}, + {"IFD/Exif", 0xa20e}, + {"IFD/Exif", 0xa20f}, + {"IFD/Exif", 0xa210}, + {"IFD/Exif", 0xa401}, + {"IFD/Exif", 0xa402}, + {"IFD/Exif", 0xa403}, + {"IFD/Exif", 0xa406}, + {"IFD/Exif", 0xa430}, + {"IFD/Exif", 0xa431}, + {"IFD/Exif", 0xa432}, + {"IFD/Exif", 0xa434}, + {"IFD/Exif", 0xa435}, + {"IFD/GPSInfo", 0x0000}, + {"IFD", 0x010f}, + {"IFD", 0x0110}, + {"IFD", 0x0112}, + {"IFD", 0x011a}, + {"IFD", 0x011b}, + {"IFD", 0x0128}, + {"IFD", 0x0132}, + {"IFD", 0x013b}, + {"IFD", 0x0213}, + {"IFD", 0x8298}, + {"IFD/Exif", 0x829a}, + {"IFD/Exif", 0x829d}, + {"IFD/Exif", 0x8822}, + {"IFD/Exif", 0x8827}, + {"IFD/Exif", 0x8830}, + {"IFD/Exif", 0x8832}, + {"IFD/Exif", 0x9000}, + {"IFD/Exif", 0x9003}, + {"IFD/Exif", 0x9004}, + {"IFD/Exif", 0x9101}, + {"IFD/Exif", 0x9201}, + {"IFD/Exif", 0x9202}, + {"IFD/Exif", 0x9204}, + {"IFD/Exif", 0x9207}, + {"IFD/Exif", 0x9209}, + {"IFD/Exif", 0x920a}, + {"IFD/Exif", 0x927c}, + {"IFD/Exif", 0x9286}, + {"IFD/Exif", 0x9290}, + {"IFD/Exif", 0x9291}, + {"IFD/Exif", 0x9292}, + {"IFD/Exif", 0xa000}, + {"IFD/Exif", 0xa001}, + {"IFD/Exif", 0xa002}, + {"IFD/Exif", 0xa003}, + {"IFD/Exif/Iop", 0x0001}, + {"IFD/Exif/Iop", 0x0002}, + {"IFD/Exif", 0xa20e}, + {"IFD/Exif", 0xa20f}, + {"IFD/Exif", 0xa210}, + {"IFD/Exif", 0xa401}, + {"IFD/Exif", 0xa402}, + {"IFD/Exif", 0xa403}, + {"IFD/Exif", 0xa406}, + {"IFD/Exif", 0xa430}, + {"IFD/Exif", 0xa431}, + {"IFD/Exif", 0xa432}, + {"IFD/Exif", 0xa434}, + {"IFD/Exif", 0xa435}, + {"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() { + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + 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 := getTestGpsImageFilepath() + + rawExif, err := SearchFileAndExtractExif(filepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, rawExif) + log.PanicIf(err) + + ifd, err := index.RootIfd.ChildWithIfdPath(exifcommon.IfdGpsInfoStandardIfdIdentity) + log.PanicIf(err) + + gi, err := ifd.GpsInfo() + log.PanicIf(err) + + fmt.Printf("%s\n", gi) + + // Output: + // GpsInfo +} + +func ExampleIfd_FindTagWithName() { + testImageFilepath := getTestImageFilepath() + + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + _, index, err := Collect(im, ti, rawExif) + log.PanicIf(err) + + tagName := "Model" + + rootIfd := index.RootIfd + + // We know the tag we want is on IFD0 (the first/root IFD). + results, err := 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 := ite.Value() + log.PanicIf(err) + + value := valueRaw.(string) + fmt.Println(value) + + // Output: + // Canon EOS 5D Mark III +} diff --git a/ifd_tag_entry.go b/ifd_tag_entry.go new file mode 100644 index 0000000..508042f --- /dev/null +++ b/ifd_tag_entry.go @@ -0,0 +1,298 @@ +package exif + +import ( + "fmt" + "io" + + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" + "b612.me/exif/undefined" +) + +var ( + iteLogger = log.NewLogger("exif.ifd_tag_entry") +) + +// IfdTagEntry refers to a tag in the loaded EXIF block. +type IfdTagEntry struct { + tagId uint16 + tagIndex int + tagType exifcommon.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 accommodate it for a consistent experience. + + ifdIdentity *exifcommon.IfdIdentity + + isUnhandledUnknown bool + + rs io.ReadSeeker + byteOrder binary.ByteOrder + + tagName string +} + +func newIfdTagEntry(ii *exifcommon.IfdIdentity, tagId uint16, tagIndex int, tagType exifcommon.TagTypePrimitive, unitCount uint32, valueOffset uint32, rawValueOffset []byte, rs io.ReadSeeker, byteOrder binary.ByteOrder) *IfdTagEntry { + return &IfdTagEntry{ + ifdIdentity: ii, + tagId: tagId, + tagIndex: tagIndex, + tagType: tagType, + unitCount: unitCount, + valueOffset: valueOffset, + rawValueOffset: rawValueOffset, + rs: rs, + byteOrder: byteOrder, + } +} + +// String returns a stringified representation of the struct. +func (ite *IfdTagEntry) String() string { + return fmt.Sprintf("IfdTagEntry", ite.ifdIdentity.String(), ite.tagId, ite.tagType.String(), ite.unitCount) +} + +// TagName returns the name of the tag. This is determined else and set after +// the parse (since it's not actually stored in the stream). If it's empty, it +// is because it is an unknown tag (nonstandard or otherwise unavailable in the +// tag-index). +func (ite *IfdTagEntry) TagName() string { + return ite.tagName +} + +// setTagName sets the tag-name. This provides the name for convenience and +// efficiency by determining it when most efficient while we're parsing rather +// than delegating it to the caller (or, worse, the user). +func (ite *IfdTagEntry) setTagName(tagName string) { + ite.tagName = tagName +} + +// IfdPath returns the fully-qualified path of the IFD that owns this tag. +func (ite *IfdTagEntry) IfdPath() string { + return ite.ifdIdentity.String() +} + +// TagId returns the ID of the tag that we represent. The combination of +// (IfdPath(), TagId()) is unique. +func (ite *IfdTagEntry) TagId() uint16 { + return ite.tagId +} + +// IsThumbnailOffset returns true if the tag has the IFD and tag-ID of a +// thumbnail offset. +func (ite *IfdTagEntry) IsThumbnailOffset() bool { + return ite.tagId == ThumbnailOffsetTagId && ite.ifdIdentity.String() == ThumbnailFqIfdPath +} + +// IsThumbnailSize returns true if the tag has the IFD and tag-ID of a thumbnail +// size. +func (ite *IfdTagEntry) IsThumbnailSize() bool { + return ite.tagId == ThumbnailSizeTagId && ite.ifdIdentity.String() == ThumbnailFqIfdPath +} + +// TagType is the type of value for this tag. +func (ite *IfdTagEntry) TagType() exifcommon.TagTypePrimitive { + return ite.tagType +} + +// updateTagType sets an alternatively interpreted tag-type. +func (ite *IfdTagEntry) updateTagType(tagType exifcommon.TagTypePrimitive) { + ite.tagType = tagType +} + +// UnitCount returns the unit-count of the tag's value. +func (ite *IfdTagEntry) UnitCount() uint32 { + return ite.unitCount +} + +// updateUnitCount sets an alternatively interpreted unit-count. +func (ite *IfdTagEntry) updateUnitCount(unitCount uint32) { + ite.unitCount = unitCount +} + +// getValueOffset is the four-byte offset converted to an integer to point to +// the location of its value in the EXIF block. The "get" parameter is obviously +// used in order to differentiate the naming of the method from the field. +func (ite *IfdTagEntry) getValueOffset() uint32 { + return ite.valueOffset +} + +// GetRawBytes renders a specific list of bytes from the value in this tag. +func (ite *IfdTagEntry) GetRawBytes() (rawBytes []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + valueContext := ite.getValueContext() + + if ite.tagType == exifcommon.TypeUndefined { + value, err := exifundefined.Decode(valueContext) + if err != nil { + if err == exifcommon.ErrUnhandledUndefinedTypedTag { + ite.setIsUnhandledUnknown(true) + return nil, exifundefined.ErrUnparseableValue + } else if err == exifundefined.ErrUnparseableValue { + return nil, err + } else { + log.Panic(err) + } + } + + // Encode it back, in order to get the raw bytes. This is the best, + // general way to do it with an undefined tag. + + rawBytes, _, err := exifundefined.Encode(value, ite.byteOrder) + log.PanicIf(err) + + return rawBytes, nil + } + + rawBytes, err = valueContext.ReadRawEncoded() + log.PanicIf(err) + + return rawBytes, nil +} + +// Value returns the specific, parsed, typed value from the tag. +func (ite *IfdTagEntry) Value() (value interface{}, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + valueContext := ite.getValueContext() + + if ite.tagType == exifcommon.TypeUndefined { + var err error + + value, err = exifundefined.Decode(valueContext) + if err != nil { + if err == exifcommon.ErrUnhandledUndefinedTypedTag || err == exifundefined.ErrUnparseableValue { + return nil, err + } + + log.Panic(err) + } + } else { + var err error + + value, err = valueContext.Values() + log.PanicIf(err) + } + + return value, nil +} + +// Format returns the tag's value as a string. +func (ite *IfdTagEntry) Format() (phrase string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + value, err := ite.Value() + if err != nil { + if err == exifcommon.ErrUnhandledUndefinedTypedTag { + return exifundefined.UnparseableUnknownTagValuePlaceholder, nil + } else if err == exifundefined.ErrUnparseableValue { + return exifundefined.UnparseableHandledTagValuePlaceholder, nil + } + + log.Panic(err) + } + + phrase, err = exifcommon.FormatFromType(value, false) + log.PanicIf(err) + + return phrase, nil +} + +// FormatFirst returns the same as Format() but only the first item. +func (ite *IfdTagEntry) FormatFirst() (phrase string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): We should add a convenience type "timestamp", to simplify translating to and from the physical ASCII and provide validation. + + value, err := ite.Value() + if err != nil { + if err == exifcommon.ErrUnhandledUndefinedTypedTag { + return exifundefined.UnparseableUnknownTagValuePlaceholder, nil + } + + log.Panic(err) + } + + phrase, err = exifcommon.FormatFromType(value, true) + log.PanicIf(err) + + return phrase, nil +} + +func (ite *IfdTagEntry) setIsUnhandledUnknown(isUnhandledUnknown bool) { + ite.isUnhandledUnknown = isUnhandledUnknown +} + +// SetChildIfd sets child-IFD information (if we represent a child IFD). +func (ite *IfdTagEntry) SetChildIfd(ii *exifcommon.IfdIdentity) { + ite.childFqIfdPath = ii.String() + ite.childIfdPath = ii.UnindexedString() + ite.childIfdName = ii.Name() +} + +// ChildIfdName returns the name of the child IFD +func (ite *IfdTagEntry) ChildIfdName() string { + return ite.childIfdName +} + +// ChildIfdPath returns the path of the child IFD. +func (ite *IfdTagEntry) ChildIfdPath() string { + return ite.childIfdPath +} + +// ChildFqIfdPath returns the complete path of the child IFD along with the +// numeric suffixes differentiating sibling occurrences of the same type. "0" +// indices are omitted. +func (ite *IfdTagEntry) ChildFqIfdPath() string { + return ite.childFqIfdPath +} + +// IfdIdentity returns the IfdIdentity associated with this tag. +func (ite *IfdTagEntry) IfdIdentity() *exifcommon.IfdIdentity { + return ite.ifdIdentity +} + +func (ite *IfdTagEntry) getValueContext() *exifcommon.ValueContext { + return exifcommon.NewValueContext( + ite.ifdIdentity.String(), + ite.tagId, + ite.unitCount, + ite.valueOffset, + ite.rawValueOffset, + ite.rs, + ite.tagType, + ite.byteOrder) +} diff --git a/ifd_tag_entry_test.go b/ifd_tag_entry_test.go new file mode 100644 index 0000000..dd8e457 --- /dev/null +++ b/ifd_tag_entry_test.go @@ -0,0 +1,85 @@ +package exif + +import ( + "bytes" + "testing" + + "github.com/dsoprea/go-logging" + "github.com/dsoprea/go-utility/v2/filesystem" + + "b612.me/exif/common" +) + +func TestIfdTagEntry_RawBytes_Allocated(t *testing.T) { + data := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} + + addressableBytes := data + sb := rifs.NewSeekableBufferWithBytes(addressableBytes) + + ite := newIfdTagEntry( + exifcommon.IfdStandardIfdIdentity, + 0x1, + 0, + exifcommon.TypeByte, + 6, + 0, + nil, + sb, + exifcommon.TestDefaultByteOrder) + + value, err := ite.GetRawBytes() + log.PanicIf(err) + + if bytes.Compare(value, data) != 0 { + t.Fatalf("Value not expected: [%s] != [%s]", value, data) + } +} + +func TestIfdTagEntry_RawBytes_Embedded(t *testing.T) { + defer func() { + if state := recover(); state != nil { + err := log.Wrap(state.(error)) + log.PrintError(err) + + t.Fatalf("Test failure.") + } + }() + + data := []byte{0x11, 0x22, 0x33, 0x44} + + ite := newIfdTagEntry( + exifcommon.IfdStandardIfdIdentity, + 0x1, + 0, + exifcommon.TypeByte, + 4, + 0, + data, + nil, + exifcommon.TestDefaultByteOrder) + + value, err := ite.GetRawBytes() + log.PanicIf(err) + + if bytes.Compare(value, data) != 0 { + t.Fatalf("Value not expected: %v != %v", value, data) + } +} + +func TestIfdTagEntry_String(t *testing.T) { + ite := newIfdTagEntry( + exifcommon.IfdStandardIfdIdentity, + 0x1, + 0, + exifcommon.TypeByte, + 6, + 0, + nil, + nil, + exifcommon.TestDefaultByteOrder) + + expected := "IfdTagEntry" + if ite.String() != expected { + t.Fatalf("string representation not expected: [%s] != [%s]", ite.String(), expected) + } +} diff --git a/package.go b/package.go new file mode 100644 index 0000000..428f74e --- /dev/null +++ b/package.go @@ -0,0 +1,8 @@ +// Package 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/tags.go b/tags.go new file mode 100644 index 0000000..824bfc9 --- /dev/null +++ b/tags.go @@ -0,0 +1,475 @@ +package exif + +import ( + "fmt" + "sync" + + "github.com/dsoprea/go-logging" + "gopkg.in/yaml.v2" + + "b612.me/exif/common" +) + +const ( + // IFD1 + + // ThumbnailFqIfdPath is the fully-qualified IFD path that the thumbnail + // must be found in. + ThumbnailFqIfdPath = "IFD1" + + // ThumbnailOffsetTagId returns the tag-ID of the thumbnail offset. + ThumbnailOffsetTagId = 0x0201 + + // ThumbnailSizeTagId returns the tag-ID of the thumbnail size. + ThumbnailSizeTagId = 0x0202 +) + +const ( + // GPS + + // TagGpsVersionId is the ID of the GPS version tag. + TagGpsVersionId = 0x0000 + + // TagLatitudeId is the ID of the GPS latitude tag. + TagLatitudeId = 0x0002 + + // TagLatitudeRefId is the ID of the GPS latitude orientation tag. + TagLatitudeRefId = 0x0001 + + // TagLongitudeId is the ID of the GPS longitude tag. + TagLongitudeId = 0x0004 + + // TagLongitudeRefId is the ID of the GPS longitude-orientation tag. + TagLongitudeRefId = 0x0003 + + // TagTimestampId is the ID of the GPS time tag. + TagTimestampId = 0x0007 + + // TagDatestampId is the ID of the GPS date tag. + TagDatestampId = 0x001d + + // TagAltitudeId is the ID of the GPS altitude tag. + TagAltitudeId = 0x0006 + + // TagAltitudeRefId is the ID of the GPS altitude-orientation tag. + 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: {}, + } +) + +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"` + TypeNames []string `yaml:"type_names"` +} + +// Indexing structures. + +// IndexedTag describes one index lookup result. +type IndexedTag struct { + // Id is the tag-ID. + Id uint16 + + // Name is the tag name. + Name string + + // IfdPath is the proper IFD path of this tag. This is not fully-qualified. + IfdPath string + + // SupportedTypes is an unsorted list of allowed tag-types. + SupportedTypes []exifcommon.TagTypePrimitive +} + +// String returns a descriptive string. +func (it *IndexedTag) String() string { + return fmt.Sprintf("TAG", it.Id, it.Name, it.IfdPath) +} + +// IsName returns true if this tag matches the given tag name. +func (it *IndexedTag) IsName(ifdPath, name string) bool { + return it.Name == name && it.IfdPath == ifdPath +} + +// Is returns true if this tag matched the given tag ID. +func (it *IndexedTag) Is(ifdPath string, id uint16) bool { + return it.Id == id && it.IfdPath == ifdPath +} + +// GetEncodingType returns the largest type that this tag's value can occupy. +func (it *IndexedTag) GetEncodingType(value interface{}) exifcommon.TagTypePrimitive { + // For convenience, we handle encoding a `time.Time` directly. + if exifcommon.IsTime(value) == true { + // Timestamps are encoded as ASCII. + value = "" + } + + if len(it.SupportedTypes) == 0 { + log.Panicf("IndexedTag [%s] (%d) has no supported types.", it.IfdPath, it.Id) + } else if len(it.SupportedTypes) == 1 { + return it.SupportedTypes[0] + } + + supportsLong := false + supportsShort := false + supportsRational := false + supportsSignedRational := false + for _, supportedType := range it.SupportedTypes { + if supportedType == exifcommon.TypeLong { + supportsLong = true + } else if supportedType == exifcommon.TypeShort { + supportsShort = true + } else if supportedType == exifcommon.TypeRational { + supportsRational = true + } else if supportedType == exifcommon.TypeSignedRational { + supportsSignedRational = true + } + } + + // We specifically check for the cases that we know to expect. + + if supportsLong == true && supportsShort == true { + return exifcommon.TypeLong + } else if supportsRational == true && supportsSignedRational == true { + if value == nil { + log.Panicf("GetEncodingType: require value to be given") + } + + if _, ok := value.(exifcommon.SignedRational); ok == true { + return exifcommon.TypeSignedRational + } + + return exifcommon.TypeRational + } + + log.Panicf("WidestSupportedType() case is not handled for tag [%s] (0x%04x): %v", it.IfdPath, it.Id, it.SupportedTypes) + return 0 +} + +// DoesSupportType returns true if this tag can be found/decoded with this type. +func (it *IndexedTag) DoesSupportType(tagType exifcommon.TagTypePrimitive) bool { + // This is always a very small collection. So, we keep it unsorted. + for _, thisTagType := range it.SupportedTypes { + if thisTagType == tagType { + return true + } + } + + return false +} + +// TagIndex is a tag-lookup facility. +type TagIndex struct { + tagsByIfd map[string]map[uint16]*IndexedTag + tagsByIfdR map[string]map[string]*IndexedTag + + mutex sync.Mutex + + doUniversalSearch bool +} + +// NewTagIndex returns a new TagIndex struct. +func NewTagIndex() *TagIndex { + ti := new(TagIndex) + + ti.tagsByIfd = make(map[string]map[uint16]*IndexedTag) + ti.tagsByIfdR = make(map[string]map[string]*IndexedTag) + + return ti +} + +// SetUniversalSearch enables a fallback to matching tags under *any* IFD. +func (ti *TagIndex) SetUniversalSearch(flag bool) { + ti.doUniversalSearch = flag +} + +// UniversalSearch enables a fallback to matching tags under *any* IFD. +func (ti *TagIndex) UniversalSearch() bool { + return ti.doUniversalSearch +} + +// Add registers a new tag to be recognized during the parse. +func (ti *TagIndex) Add(it *IndexedTag) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + ti.mutex.Lock() + defer ti.mutex.Unlock() + + // 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 +} + +func (ti *TagIndex) getOne(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) + } + + ti.mutex.Lock() + defer ti.mutex.Unlock() + + family, found := ti.tagsByIfd[ifdPath] + if found == false { + return nil, ErrTagNotFound + } + + it, found = family[id] + if found == false { + return nil, ErrTagNotFound + } + + return it, nil +} + +// Get returns information about the non-IFD tag given a tag ID. `ifdPath` must +// not be fully-qualified. +func (ti *TagIndex) Get(ii *exifcommon.IfdIdentity, id uint16) (it *IndexedTag, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + ifdPath := ii.UnindexedString() + + it, err = ti.getOne(ifdPath, id) + if err == nil { + return it, nil + } else if err != ErrTagNotFound { + log.Panic(err) + } + + if ti.doUniversalSearch == false { + return nil, ErrTagNotFound + } + + // We've been told to fallback to look for the tag in other IFDs. + + skipIfdPath := ii.UnindexedString() + + for currentIfdPath, _ := range ti.tagsByIfd { + if currentIfdPath == skipIfdPath { + // Skip the primary IFD, which has already been checked. + continue + } + + it, err = ti.getOne(currentIfdPath, id) + if err == nil { + tagsLogger.Warningf(nil, + "Found tag (0x%02x) in the wrong IFD: [%s] != [%s]", + id, currentIfdPath, ifdPath) + + return it, nil + } else if err != ErrTagNotFound { + log.Panic(err) + } + } + + return nil, ErrTagNotFound +} + +var ( + // tagGuessDefaultIfdIdentities describes which IFDs we'll look for a given + // tag-ID in, if it's not found where it's supposed to be. We suppose that + // Exif-IFD tags might be found in IFD0 or IFD1, or IFD0/IFD1 tags might be + // found in the Exif IFD. This is the only thing we've seen so far. So, this + // is the limit of our guessing. + tagGuessDefaultIfdIdentities = []*exifcommon.IfdIdentity{ + exifcommon.IfdExifStandardIfdIdentity, + exifcommon.IfdStandardIfdIdentity, + } +) + +// FindFirst looks for the given tag-ID in each of the given IFDs in the given +// order. If `fqIfdPaths` is `nil` then use a default search order. This defies +// the standard, which requires each tag to exist in certain IFDs. This is a +// contingency to make recommendations for malformed data. +// +// Things *can* end badly here, in that the same tag-ID in different IFDs might +// describe different data and different ata-types, and our decode might then +// produce binary and non-printable data. +func (ti *TagIndex) FindFirst(id uint16, typeId exifcommon.TagTypePrimitive, ifdIdentities []*exifcommon.IfdIdentity) (it *IndexedTag, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if ifdIdentities == nil { + ifdIdentities = tagGuessDefaultIfdIdentities + } + + for _, ii := range ifdIdentities { + it, err := ti.Get(ii, id) + if err != nil { + if err == ErrTagNotFound { + continue + } + + log.Panic(err) + } + + // Even though the tag might be mislocated, the type should still be the + // same. Check this so we don't accidentally end-up on a complete + // irrelevant tag with a totally different data type. This attempts to + // mitigate producing garbage. + for _, supportedType := range it.SupportedTypes { + if supportedType == typeId { + return it, nil + } + } + } + + return nil, ErrTagNotFound +} + +// GetWithName returns information about the non-IFD tag given a tag name. +func (ti *TagIndex) GetWithName(ii *exifcommon.IfdIdentity, 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) + } + + ifdPath := ii.UnindexedString() + + 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 + tagTypeNames := tagInfo.TypeNames + + if tagTypeNames == nil { + if tagTypeName == "" { + log.Panicf("no tag-types were given when registering standard tag [%s] (0x%04x) [%s]", ifdPath, tagId, tagName) + } + + tagTypeNames = []string{ + tagTypeName, + } + } else if tagTypeName != "" { + log.Panicf("both 'type_names' and 'type_name' were given when registering standard tag [%s] (0x%04x) [%s]", ifdPath, tagId, tagName) + } + + tagTypes := make([]exifcommon.TagTypePrimitive, 0) + for _, tagTypeName := range tagTypeNames { + + // TODO(dustin): Discard unsupported types. This helps us with non-standard types that have actually been found in real data, that we ignore for right now. e.g. SSHORT, FLOAT, DOUBLE + tagTypeId, found := exifcommon.GetTypeByName(tagTypeName) + if found == false { + tagsLogger.Warningf(nil, "Type [%s] for tag [%s] being loaded is not valid and is being ignored.", tagTypeName, tagName) + continue + } + + tagTypes = append(tagTypes, tagTypeId) + } + + if len(tagTypes) == 0 { + tagsLogger.Warningf(nil, "Tag [%s] (0x%04x) [%s] being loaded does not have any supported types and will not be registered.", ifdPath, tagId, tagName) + continue + } + + it := &IndexedTag{ + IfdPath: ifdPath, + Id: tagId, + Name: tagName, + SupportedTypes: tagTypes, + } + + err = ti.Add(it) + log.PanicIf(err) + + count++ + } + } + + tagsLogger.Debugf(nil, "(%d) tags loaded.", count) + + return nil +} diff --git a/tags_data.go b/tags_data.go new file mode 100644 index 0000000..dcf0cc4 --- /dev/null +++ b/tags_data.go @@ -0,0 +1,968 @@ +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: 0x9010 + name: OffsetTime + type_name: ASCII +- id: 0x9011 + name: OffsetTimeOriginal + type_name: ASCII +- id: 0x9012 + name: OffsetTimeDigitized + 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_names: [LONG, SHORT] +- id: 0xa003 + name: PixelYDimension + type_names: [LONG, SHORT] +- 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_names: [LONG, SHORT] +- id: 0x0101 + name: ImageLength + type_names: [LONG, SHORT] +- 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_names: [LONG, SHORT] +- id: 0x0112 + name: Orientation + type_name: SHORT +- id: 0x0115 + name: SamplesPerPixel + type_name: SHORT +- id: 0x0116 + name: RowsPerStrip + type_names: [LONG, SHORT] +- id: 0x0117 + name: StripByteCounts + type_names: [LONG, SHORT] +- 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: 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 +# NOTE(dustin): SRATIONAL isn't mentioned in the standard, but we have seen it in real data. + type_names: [RATIONAL, SRATIONAL] +- id: 0x829d + name: FNumber +# NOTE(dustin): SRATIONAL isn't mentioned in the standard, but we have seen it in real data. + type_names: [RATIONAL, SRATIONAL] +- 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: 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: 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: 0xc740 + name: OpcodeList1 + type_name: UNDEFINED +- id: 0xc741 + name: OpcodeList2 + type_name: UNDEFINED +- id: 0xc74e + name: OpcodeList3 + type_name: UNDEFINED +# This tag may be used to specify the size of raster pixel spacing in the +# model space units, when the raster space can be embedded in the model space +# coordinate system without rotation, and consists of the following 3 values: +# ModelPixelScaleTag = (ScaleX, ScaleY, ScaleZ) +# where ScaleX and ScaleY give the horizontal and vertical spacing of raster +# pixels. The ScaleZ is primarily used to map the pixel value of a digital +# elevation model into the correct Z-scale, and so for most other purposes +# this value should be zero (since most model spaces are 2-D, with Z=0). +# Source: http://geotiff.maptools.org/spec/geotiff2.6.html#2.6.1 +- id: 0x830e + name: ModelPixelScaleTag + type_name: DOUBLE +# This tag stores raster->model tiepoint pairs in the order +# ModelTiepointTag = (...,I,J,K, X,Y,Z...), +# where (I,J,K) is the point at location (I,J) in raster space with +# pixel-value K, and (X,Y,Z) is a vector in model space. In most cases the +# model space is only two-dimensional, in which case both K and Z should be +# set to zero; this third dimension is provided in anticipation of future +# support for 3D digital elevation models and vertical coordinate systems. +# Source: http://geotiff.maptools.org/spec/geotiff2.6.html#2.6.1 +- id: 0x8482 + name: ModelTiepointTag + type_name: DOUBLE +# This tag may be used to specify the transformation matrix between the +# raster space (and its dependent pixel-value space) and the (possibly 3D) +# model space. +# Source: http://geotiff.maptools.org/spec/geotiff2.6.html#2.6.1 +- id: 0x85d8 + name: ModelTransformationTag + 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/tags_data_test.go b/tags_data_test.go new file mode 100644 index 0000000..3becf34 --- /dev/null +++ b/tags_data_test.go @@ -0,0 +1,33 @@ +package exif + +import ( + "testing" + + "github.com/dsoprea/go-logging" +) + +func TestGeotiffTags(t *testing.T) { + testImageFilepath := getTestGeotiffFilepath() + + // Returns a slice starting with the EXIF data and going to the end of the + // image. + rawExif, err := SearchFileAndExtractExif(testImageFilepath) + log.PanicIf(err) + + exifTags, _, err := GetFlatExifData(rawExif, nil) + log.PanicIf(err) + + exifTagsIDMap := make(map[uint16]int) + + for _, e := range exifTags { + exifTagsIDMap[e.TagId] = 1 + } + + if exifTagsIDMap[0x830e] == 0 { + t.Fatal("Missing ModelPixelScaleTag.") + } + + if exifTagsIDMap[0x8482] == 0 { + t.Fatal("Missing ModelTiepointTag.") + } +} diff --git a/tags_test.go b/tags_test.go new file mode 100644 index 0000000..0172bbb --- /dev/null +++ b/tags_test.go @@ -0,0 +1,403 @@ +package exif + +import ( + "reflect" + "testing" + "time" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +func TestIndexedTag_String(t *testing.T) { + it := &IndexedTag{ + Id: 0xb, + Name: "some_name", + IfdPath: "ifd/path", + SupportedTypes: []exifcommon.TagTypePrimitive{ + exifcommon.TagTypePrimitive(11), + exifcommon.TagTypePrimitive(22), + }, + } + + if it.String() != "TAG" { + t.Fatalf("String output not correct: [%s]", it.String()) + } +} + +func TestIndexedTag_IsName_True(t *testing.T) { + it := &IndexedTag{ + Name: "some_name", + IfdPath: "ifd/path", + } + + if it.IsName("ifd/path", "some_name") != true { + t.Fatalf("IsName is not true.") + } +} + +func TestIndexedTag_IsName_FalseOnName(t *testing.T) { + it := &IndexedTag{ + Name: "some_name", + IfdPath: "ifd/path", + } + + if it.IsName("ifd/path", "some_name2") != false { + t.Fatalf("IsName is not false.") + } +} + +func TestIndexedTag_IsName_FalseOnIfdPath(t *testing.T) { + it := &IndexedTag{ + Name: "some_name", + IfdPath: "ifd/path", + } + + if it.IsName("ifd/path2", "some_name") != false { + t.Fatalf("IsName is not false.") + } +} + +func TestIndexedTag_Is_True(t *testing.T) { + it := &IndexedTag{ + Id: 0x11, + IfdPath: "ifd/path", + } + + if it.Is("ifd/path", 0x11) != true { + t.Fatalf("Is is not true.") + } +} + +func TestIndexedTag_Is_FalseOnId(t *testing.T) { + it := &IndexedTag{ + Id: 0x11, + IfdPath: "ifd/path", + } + + if it.Is("ifd/path", 0x12) != false { + t.Fatalf("Is is not false.") + } +} + +func TestIndexedTag_Is_FalseOnIfdName(t *testing.T) { + it := &IndexedTag{ + Id: 0x11, + IfdPath: "ifd/path", + } + + if it.Is("ifd/path2", 0x11) != false { + t.Fatalf("Is is not false.") + } +} + +func TestIndexedTag_GetEncodingType_WorksWithOneType(t *testing.T) { + it := &IndexedTag{ + Id: 0xb, + Name: "some_name", + IfdPath: "ifd/path", + SupportedTypes: []exifcommon.TagTypePrimitive{ + exifcommon.TypeRational, + }, + } + + if it.GetEncodingType(nil) != exifcommon.TypeRational { + t.Fatalf("Expected the one type that was set.") + } +} + +func TestIndexedTag_GetEncodingType_FailsOnEmpty(t *testing.T) { + // This also looks for an empty to reference the first spot, invalidly. + + defer func() { + errRaw := recover() + if errRaw == nil { + t.Fatalf("Expected failure due to empty supported-types.") + } + + err := errRaw.(error) + if err.Error() != "IndexedTag [] (0) has no supported types." { + log.Panic(err) + } + }() + + it := &IndexedTag{ + SupportedTypes: []exifcommon.TagTypePrimitive{}, + } + + it.GetEncodingType(nil) +} + +func TestIndexedTag_GetEncodingType_PreferLongOverShort(t *testing.T) { + it := &IndexedTag{ + Id: 0xb, + Name: "some_name", + IfdPath: "ifd/path", + SupportedTypes: []exifcommon.TagTypePrimitive{ + exifcommon.TypeShort, + }, + } + + if it.GetEncodingType(nil) != exifcommon.TypeShort { + t.Fatalf("Expected the second (LONG) type to be returned.") + } + + it = &IndexedTag{ + Id: 0xb, + Name: "some_name", + IfdPath: "ifd/path", + SupportedTypes: []exifcommon.TagTypePrimitive{ + exifcommon.TypeShort, + exifcommon.TypeLong, + }, + } + + if it.GetEncodingType(nil) != exifcommon.TypeLong { + t.Fatalf("Expected the second (LONG) type to be returned.") + } +} + +func TestIndexedTag_GetEncodingType_BothRationalTypes(t *testing.T) { + it := &IndexedTag{ + Id: 0xb, + Name: "some_name", + IfdPath: "ifd/path", + SupportedTypes: []exifcommon.TagTypePrimitive{ + exifcommon.TypeRational, + exifcommon.TypeSignedRational, + }, + } + + v1 := exifcommon.Rational{} + + if it.GetEncodingType(v1) != exifcommon.TypeRational { + t.Fatalf("Expected the second (RATIONAL) type to be returned.") + } + + v2 := exifcommon.SignedRational{} + + if it.GetEncodingType(v2) != exifcommon.TypeSignedRational { + t.Fatalf("Expected the second (SIGNED RATIONAL) type to be returned.") + } +} + +func TestIndexedTag_GetEncodingType_Timestamp(t *testing.T) { + it := &IndexedTag{ + SupportedTypes: []exifcommon.TagTypePrimitive{ + exifcommon.TypeAscii, + }, + } + + zeroTime := time.Time{} + + if it.GetEncodingType(zeroTime) != exifcommon.TypeAscii { + t.Fatalf("Expected the timestamp to to be encoded as ASCII.") + } +} + +func TestIndexedTag_DoesSupportType(t *testing.T) { + it := &IndexedTag{ + Id: 0xb, + Name: "some_name", + IfdPath: "ifd/path", + SupportedTypes: []exifcommon.TagTypePrimitive{ + exifcommon.TypeRational, + exifcommon.TypeSignedRational, + }, + } + + if it.DoesSupportType(exifcommon.TypeRational) != true { + t.Fatalf("Does not support unsigned-rational.") + } else if it.DoesSupportType(exifcommon.TypeSignedRational) != true { + t.Fatalf("Does not support signed-rational.") + } else if it.DoesSupportType(exifcommon.TypeLong) != false { + t.Fatalf("Does not support long.") + } +} + +func TestNewTagIndex(t *testing.T) { + ti := NewTagIndex() + + if ti.tagsByIfd == nil { + t.Fatalf("tagsByIfd is nil.") + } else if ti.tagsByIfdR == nil { + t.Fatalf("tagsByIfdR is nil.") + } +} + +func TestTagIndex_Add(t *testing.T) { + ti := NewTagIndex() + + if len(ti.tagsByIfd) != 0 { + t.Fatalf("tagsByIfd should be empty initially.") + } else if len(ti.tagsByIfdR) != 0 { + t.Fatalf("tagsByIfdR should be empty initially.") + } + + it := &IndexedTag{ + Id: 0xb, + Name: "some_name", + IfdPath: "ifd/path", + SupportedTypes: []exifcommon.TagTypePrimitive{ + exifcommon.TypeRational, + exifcommon.TypeSignedRational, + }, + } + + err := ti.Add(it) + log.PanicIf(err) + + if reflect.DeepEqual(ti.tagsByIfd[it.IfdPath][it.Id], it) != true { + t.Fatalf("Not present in forward lookup.") + } else if reflect.DeepEqual(ti.tagsByIfdR[it.IfdPath][it.Name], it) != true { + t.Fatalf("Not present in reverse lookup.") + } +} + +func TestTagIndex_Get(t *testing.T) { + ti := NewTagIndex() + + it, err := ti.Get(exifcommon.IfdStandardIfdIdentity, 0x10f) + log.PanicIf(err) + + if it.Is(exifcommon.IfdStandardIfdIdentity.UnindexedString(), 0x10f) == false || it.IsName(exifcommon.IfdStandardIfdIdentity.UnindexedString(), "Make") == false { + t.Fatalf("tag info not correct") + } +} + +func TestTagIndex_GetWithName(t *testing.T) { + ti := NewTagIndex() + + it, err := ti.GetWithName(exifcommon.IfdStandardIfdIdentity, "Make") + log.PanicIf(err) + + if it.Is(exifcommon.IfdStandardIfdIdentity.UnindexedString(), 0x10f) == false { + t.Fatalf("tag info not correct") + } +} + +func TestTagIndex_FindFirst_HitOnFirst(t *testing.T) { + + searchOrder := []*exifcommon.IfdIdentity{ + exifcommon.IfdExifStandardIfdIdentity, + exifcommon.IfdStandardIfdIdentity, + } + + ti := NewTagIndex() + + // ExifVersion + it, err := ti.FindFirst(0x9000, exifcommon.TypeUndefined, searchOrder) + log.PanicIf(err) + + if it.Is("IFD/Exif", 0x9000) != true { + t.Fatalf("Returned tag is not correct.") + } +} + +func TestTagIndex_FindFirst_HitOnSecond(t *testing.T) { + + searchOrder := []*exifcommon.IfdIdentity{ + exifcommon.IfdExifStandardIfdIdentity, + exifcommon.IfdStandardIfdIdentity, + } + + ti := NewTagIndex() + + // ProcessingSoftware + it, err := ti.FindFirst(0x000b, exifcommon.TypeAscii, searchOrder) + log.PanicIf(err) + + if it.Is("IFD", 0x000b) != true { + t.Fatalf("Returned tag is not correct.") + } +} + +func TestTagIndex_FindFirst_DefaultOrder_Miss(t *testing.T) { + + searchOrder := []*exifcommon.IfdIdentity{ + exifcommon.IfdExifStandardIfdIdentity, + exifcommon.IfdStandardIfdIdentity, + } + + ti := NewTagIndex() + + _, err := ti.FindFirst(0x1234, exifcommon.TypeRational, searchOrder) + if err == nil { + t.Fatalf("Expected error for invalid tag.") + } else if err != ErrTagNotFound { + log.Panic(err) + } +} + +func TestTagIndex_FindFirst_ReverseDefaultOrder_HitOnSecond(t *testing.T) { + + reverseSearchOrder := []*exifcommon.IfdIdentity{ + exifcommon.IfdStandardIfdIdentity, + exifcommon.IfdExifStandardIfdIdentity, + } + + ti := NewTagIndex() + + // ExifVersion + it, err := ti.FindFirst(0x9000, exifcommon.TypeUndefined, reverseSearchOrder) + log.PanicIf(err) + + if it.Is("IFD/Exif", 0x9000) != true { + t.Fatalf("Returned tag is not correct.") + } +} + +func TestTagIndex_FindFirst_ReverseDefaultOrder_HitOnFirst(t *testing.T) { + + reverseSearchOrder := []*exifcommon.IfdIdentity{ + exifcommon.IfdStandardIfdIdentity, + exifcommon.IfdExifStandardIfdIdentity, + } + + ti := NewTagIndex() + + // ProcessingSoftware + it, err := ti.FindFirst(0x000b, exifcommon.TypeAscii, reverseSearchOrder) + log.PanicIf(err) + + if it.Is("IFD", 0x000b) != true { + t.Fatalf("Returned tag is not correct.") + } +} + +func TestTagIndex_FindFirst_ReverseDefaultOrder_Miss(t *testing.T) { + + reverseSearchOrder := []*exifcommon.IfdIdentity{ + exifcommon.IfdStandardIfdIdentity, + exifcommon.IfdExifStandardIfdIdentity, + } + + ti := NewTagIndex() + + _, err := ti.FindFirst(0x1234, exifcommon.TypeRational, reverseSearchOrder) + if err == nil { + t.Fatalf("Expected error for invalid tag.") + } else if err != ErrTagNotFound { + log.Panic(err) + } +} + +func TestLoadStandardTags(t *testing.T) { + ti := NewTagIndex() + + if len(ti.tagsByIfd) != 0 { + t.Fatalf("tagsByIfd should be empty initially.") + } else if len(ti.tagsByIfdR) != 0 { + t.Fatalf("tagsByIfdR should be empty initially.") + } + + err := LoadStandardTags(ti) + log.PanicIf(err) + + if len(ti.tagsByIfd) == 0 { + t.Fatalf("tagsByIfd should be non-empty at the end.") + } else if len(ti.tagsByIfdR) == 0 { + t.Fatalf("tagsByIfdR should be non-empty at the end.") + } +} diff --git a/testing_common.go b/testing_common.go new file mode 100644 index 0000000..ab395ee --- /dev/null +++ b/testing_common.go @@ -0,0 +1,188 @@ +package exif + +import ( + "path" + "reflect" + "testing" + + "io/ioutil" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +var ( + testExifData []byte +) + +func getExifSimpleTestIb() *IfdBuilder { + defer func() { + if state := recover(); state != nil { + err := log.Wrap(state.(error)) + log.Panic(err) + } + }() + + im := exifcommon.NewIfdMapping() + + err := exifcommon.LoadStandardIfds(im) + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.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, []exifcommon.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 := exifcommon.NewIfdMapping() + + err := exifcommon.LoadStandardIfds(im) + log.PanicIf(err) + + ti := NewTagIndex() + ib := NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity, exifcommon.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, []exifcommon.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 := exifcommon.NewIfdMapping() + + err := exifcommon.LoadStandardIfds(im) + log.PanicIf(err) + + ti := NewTagIndex() + + eh, index, err := Collect(im, ti, exifData) + log.PanicIf(err) + + if eh.ByteOrder != exifcommon.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() != exifcommon.TestDefaultByteOrder { + t.Fatalf("IFD byte-order not correct.") + } else if ifd.ifdIdentity.UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() { + t.Fatalf("IFD name not correct.") + } else if ifd.ifdIdentity.Index() != 0 { + t.Fatalf("IFD index not zero: (%d)", ifd.ifdIdentity.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, original types (this is awesome). + + expected := []struct { + tagId uint16 + value interface{} + }{ + {tagId: 0x000b, value: "asciivalue"}, + {tagId: 0x00ff, value: []uint16{0x1122}}, + {tagId: 0x0100, value: []uint32{0x33445566}}, + {tagId: 0x013e, value: []exifcommon.Rational{{Numerator: 0x11112222, Denominator: 0x33334444}}}, + } + + for i, ite := range ifd.Entries() { + if ite.TagId() != expected[i].tagId { + t.Fatalf("Tag-ID for entry (%d) not correct: (0x%02x) != (0x%02x)", i, ite.TagId(), expected[i].tagId) + } + + value, err := ite.Value() + 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 getTestImageFilepath() string { + assetsPath := exifcommon.GetTestAssetsPath() + testImageFilepath := path.Join(assetsPath, "NDM_8901.jpg") + return testImageFilepath +} + +func getTestExifData() []byte { + if testExifData == nil { + assetsPath := exifcommon.GetTestAssetsPath() + filepath := path.Join(assetsPath, "NDM_8901.jpg.exif") + + var err error + + testExifData, err = ioutil.ReadFile(filepath) + log.PanicIf(err) + } + + return testExifData +} + +func getTestGpsImageFilepath() string { + assetsPath := exifcommon.GetTestAssetsPath() + testGpsImageFilepath := path.Join(assetsPath, "gps.jpg") + return testGpsImageFilepath +} + +func getTestGeotiffFilepath() string { + assetsPath := exifcommon.GetTestAssetsPath() + testGeotiffFilepath := path.Join(assetsPath, "geotiff_example.tif") + return testGeotiffFilepath +} diff --git a/undefined/README.md b/undefined/README.md new file mode 100644 index 0000000..d2caa6e --- /dev/null +++ b/undefined/README.md @@ -0,0 +1,4 @@ + +## 0xa40b + +The specification is not specific/clear enough to be handled. Without a working example ,we're deferring until some point in the future when either we or someone else has a better understanding. diff --git a/undefined/accessor.go b/undefined/accessor.go new file mode 100644 index 0000000..2f0e300 --- /dev/null +++ b/undefined/accessor.go @@ -0,0 +1,62 @@ +package exifundefined + +import ( + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +// Encode encodes the given encodeable undefined value to bytes. +func Encode(value EncodeableValue, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + encoderName := value.EncoderName() + + encoder, found := encoders[encoderName] + if found == false { + log.Panicf("no encoder registered for type [%s]", encoderName) + } + + encoded, unitCount, err = encoder.Encode(value, byteOrder) + log.PanicIf(err) + + return encoded, unitCount, nil +} + +// Decode constructs a value from raw encoded bytes +func Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + uth := UndefinedTagHandle{ + IfdPath: valueContext.IfdPath(), + TagId: valueContext.TagId(), + } + + decoder, found := decoders[uth] + if found == false { + // 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, exifcommon.ErrUnhandledUndefinedTypedTag + } + + value, err = decoder.Decode(valueContext) + if err != nil { + if err == ErrUnparseableValue { + return nil, err + } + + log.Panic(err) + } + + return value, nil +} diff --git a/undefined/exif_8828_oecf.go b/undefined/exif_8828_oecf.go new file mode 100644 index 0000000..a11ed17 --- /dev/null +++ b/undefined/exif_8828_oecf.go @@ -0,0 +1,148 @@ +package exifundefined + +import ( + "bytes" + "fmt" + + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +type Tag8828Oecf struct { + Columns uint16 + Rows uint16 + ColumnNames []string + Values []exifcommon.SignedRational +} + +func (oecf Tag8828Oecf) String() string { + return fmt.Sprintf("Tag8828Oecf", oecf.Columns, oecf.Rows) +} + +func (oecf Tag8828Oecf) EncoderName() string { + return "Codec8828Oecf" +} + +type Codec8828Oecf struct { +} + +func (Codec8828Oecf) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test + + oecf, ok := value.(Tag8828Oecf) + if ok == false { + log.Panicf("can only encode a Tag8828Oecf") + } + + b := new(bytes.Buffer) + + err = binary.Write(b, byteOrder, oecf.Columns) + log.PanicIf(err) + + err = binary.Write(b, byteOrder, oecf.Rows) + log.PanicIf(err) + + for _, name := range oecf.ColumnNames { + _, err := b.Write([]byte(name)) + log.PanicIf(err) + + _, err = b.Write([]byte{0}) + log.PanicIf(err) + } + + ve := exifcommon.NewValueEncoder(byteOrder) + + ed, err := ve.Encode(oecf.Values) + log.PanicIf(err) + + _, err = b.Write(ed.Encoded) + log.PanicIf(err) + + return b.Bytes(), uint32(b.Len()), nil +} + +func (Codec8828Oecf) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test using known good data. + + valueContext.SetUndefinedValueType(exifcommon.TypeByte) + + valueBytes, err := valueContext.ReadBytes() + log.PanicIf(err) + + oecf := Tag8828Oecf{} + + oecf.Columns = valueContext.ByteOrder().Uint16(valueBytes[0:2]) + oecf.Rows = valueContext.ByteOrder().Uint16(valueBytes[2:4]) + + columnNames := make([]string, oecf.Columns) + + // startAt is where the current column name starts. + startAt := 4 + + // offset is our current position. + offset := startAt + + currentColumnNumber := uint16(0) + + for currentColumnNumber < oecf.Columns { + if valueBytes[offset] == 0 { + columnName := string(valueBytes[startAt:offset]) + if len(columnName) == 0 { + log.Panicf("SFR column (%d) has zero length", currentColumnNumber) + } + + columnNames[currentColumnNumber] = columnName + currentColumnNumber++ + + offset++ + startAt = offset + continue + } + + offset++ + } + + oecf.ColumnNames = columnNames + + rawRationalBytes := valueBytes[offset:] + + rationalSize := exifcommon.TypeSignedRational.Size() + if len(rawRationalBytes)%rationalSize > 0 { + log.Panicf("OECF signed-rationals not aligned: (%d) %% (%d) > 0", len(rawRationalBytes), rationalSize) + } + + rationalCount := len(rawRationalBytes) / rationalSize + + parser := new(exifcommon.Parser) + + byteOrder := valueContext.ByteOrder() + + items, err := parser.ParseSignedRationals(rawRationalBytes, uint32(rationalCount), byteOrder) + log.PanicIf(err) + + oecf.Values = items + + return oecf, nil +} + +func init() { + registerDecoder( + exifcommon.IfdExifStandardIfdIdentity.UnindexedString(), + 0x8828, + Codec8828Oecf{}) +} diff --git a/undefined/exif_8828_oecf_test.go b/undefined/exif_8828_oecf_test.go new file mode 100644 index 0000000..c87efcc --- /dev/null +++ b/undefined/exif_8828_oecf_test.go @@ -0,0 +1,90 @@ +package exifundefined + +import ( + "bytes" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + "github.com/dsoprea/go-utility/v2/filesystem" + + "b612.me/exif/common" +) + +func TestTag8828Oecf_String(t *testing.T) { + ut := Tag8828Oecf{ + Columns: 11, + Rows: 22, + } + + s := ut.String() + + if s != "Tag8828Oecf" { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestCodec8828Oecf_Encode(t *testing.T) { + ut := Tag8828Oecf{ + Columns: 2, + Rows: 22, + ColumnNames: []string{"aa", "bb"}, + Values: []exifcommon.SignedRational{{11, 22}}, + } + + codec := Codec8828Oecf{} + + encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder) + log.PanicIf(err) + + expectedBytes := []byte{ + 0x00, 0x02, + 0x00, 0x16, + 0x61, 0x61, 0x00, 0x62, 0x62, 0x00, + 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x16} + + if bytes.Equal(encoded, expectedBytes) != true { + exifcommon.DumpBytesClause(encoded) + + t.Fatalf("Encoded bytes not correct.") + } else if unitCount != 18 { + t.Fatalf("Unit-count not correct: (%d)", unitCount) + } +} + +func TestCodec8828Oecf_Decode(t *testing.T) { + encoded := []byte{ + 0x00, 0x02, + 0x00, 0x16, + 0x61, 0x61, 0x00, 0x62, 0x62, 0x00, + 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x16} + + addressableData := encoded + sb := rifs.NewSeekableBufferWithBytes(addressableData) + + valueContext := exifcommon.NewValueContext( + "", + 0, + uint32(len(encoded)), + 0, + nil, + sb, + exifcommon.TypeUndefined, + exifcommon.TestDefaultByteOrder) + + codec := Codec8828Oecf{} + + value, err := codec.Decode(valueContext) + log.PanicIf(err) + + expectedValue := Tag8828Oecf{ + Columns: 2, + Rows: 22, + ColumnNames: []string{"aa", "bb"}, + Values: []exifcommon.SignedRational{{11, 22}}, + } + + if reflect.DeepEqual(value, expectedValue) != true { + t.Fatalf("Decoded value not correct: %s", value) + } +} diff --git a/undefined/exif_9000_exif_version.go b/undefined/exif_9000_exif_version.go new file mode 100644 index 0000000..eda7422 --- /dev/null +++ b/undefined/exif_9000_exif_version.go @@ -0,0 +1,69 @@ +package exifundefined + +import ( + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +type Tag9000ExifVersion struct { + ExifVersion string +} + +func (Tag9000ExifVersion) EncoderName() string { + return "Codec9000ExifVersion" +} + +func (ev Tag9000ExifVersion) String() string { + return ev.ExifVersion +} + +type Codec9000ExifVersion struct { +} + +func (Codec9000ExifVersion) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + s, ok := value.(Tag9000ExifVersion) + if ok == false { + log.Panicf("can only encode a Tag9000ExifVersion") + } + + return []byte(s.ExifVersion), uint32(len(s.ExifVersion)), nil +} + +func (Codec9000ExifVersion) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + valueContext.SetUndefinedValueType(exifcommon.TypeAsciiNoNul) + + valueString, err := valueContext.ReadAsciiNoNul() + log.PanicIf(err) + + ev := Tag9000ExifVersion{ + ExifVersion: valueString, + } + + return ev, nil +} + +func init() { + registerEncoder( + Tag9000ExifVersion{}, + Codec9000ExifVersion{}) + + registerDecoder( + exifcommon.IfdExifStandardIfdIdentity.UnindexedString(), + 0x9000, + Codec9000ExifVersion{}) +} diff --git a/undefined/exif_9000_exif_version_test.go b/undefined/exif_9000_exif_version_test.go new file mode 100644 index 0000000..777c14b --- /dev/null +++ b/undefined/exif_9000_exif_version_test.go @@ -0,0 +1,64 @@ +package exifundefined + +import ( + "bytes" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +func TestTag9000ExifVersion_String(t *testing.T) { + ut := Tag9000ExifVersion{"abc"} + s := ut.String() + + if s != "abc" { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestCodec9000ExifVersion_Encode(t *testing.T) { + s := "abc" + ut := Tag9000ExifVersion{s} + + codec := Codec9000ExifVersion{} + + encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder) + log.PanicIf(err) + + if bytes.Equal(encoded, []byte(s)) != true { + t.Fatalf("Encoded bytes not correct: %v", encoded) + } else if unitCount != uint32(len(s)) { + t.Fatalf("Unit-count not correct: (%d)", unitCount) + } +} + +func TestCodec9000ExifVersion_Decode(t *testing.T) { + s := "abc" + ut := Tag9000ExifVersion{s} + + encoded := []byte(s) + + rawValueOffset := encoded + + valueContext := exifcommon.NewValueContext( + "", + 0, + uint32(len(encoded)), + 0, + rawValueOffset, + nil, + exifcommon.TypeUndefined, + exifcommon.TestDefaultByteOrder) + + codec := Codec9000ExifVersion{} + + value, err := codec.Decode(valueContext) + log.PanicIf(err) + + if reflect.DeepEqual(value, ut) != true { + t.Fatalf("Decoded value not correct: %s\n", value) + } +} diff --git a/undefined/exif_9101_components_configuration.go b/undefined/exif_9101_components_configuration.go new file mode 100644 index 0000000..d0b12d9 --- /dev/null +++ b/undefined/exif_9101_components_configuration.go @@ -0,0 +1,124 @@ +package exifundefined + +import ( + "bytes" + "fmt" + + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +const ( + TagUndefinedType_9101_ComponentsConfiguration_Channel_Y = 0x1 + TagUndefinedType_9101_ComponentsConfiguration_Channel_Cb = 0x2 + TagUndefinedType_9101_ComponentsConfiguration_Channel_Cr = 0x3 + TagUndefinedType_9101_ComponentsConfiguration_Channel_R = 0x4 + TagUndefinedType_9101_ComponentsConfiguration_Channel_G = 0x5 + TagUndefinedType_9101_ComponentsConfiguration_Channel_B = 0x6 +) + +const ( + TagUndefinedType_9101_ComponentsConfiguration_OTHER = iota + TagUndefinedType_9101_ComponentsConfiguration_RGB = iota + TagUndefinedType_9101_ComponentsConfiguration_YCBCR = iota +) + +var ( + TagUndefinedType_9101_ComponentsConfiguration_Names = map[int]string{ + TagUndefinedType_9101_ComponentsConfiguration_OTHER: "OTHER", + TagUndefinedType_9101_ComponentsConfiguration_RGB: "RGB", + TagUndefinedType_9101_ComponentsConfiguration_YCBCR: "YCBCR", + } + + TagUndefinedType_9101_ComponentsConfiguration_Configurations = map[int][]byte{ + TagUndefinedType_9101_ComponentsConfiguration_RGB: { + TagUndefinedType_9101_ComponentsConfiguration_Channel_R, + TagUndefinedType_9101_ComponentsConfiguration_Channel_G, + TagUndefinedType_9101_ComponentsConfiguration_Channel_B, + 0, + }, + + TagUndefinedType_9101_ComponentsConfiguration_YCBCR: { + TagUndefinedType_9101_ComponentsConfiguration_Channel_Y, + TagUndefinedType_9101_ComponentsConfiguration_Channel_Cb, + TagUndefinedType_9101_ComponentsConfiguration_Channel_Cr, + 0, + }, + } +) + +type TagExif9101ComponentsConfiguration struct { + ConfigurationId int + ConfigurationBytes []byte +} + +func (TagExif9101ComponentsConfiguration) EncoderName() string { + return "CodecExif9101ComponentsConfiguration" +} + +func (cc TagExif9101ComponentsConfiguration) String() string { + return fmt.Sprintf("Exif9101ComponentsConfiguration", TagUndefinedType_9101_ComponentsConfiguration_Names[cc.ConfigurationId], cc.ConfigurationBytes) +} + +type CodecExif9101ComponentsConfiguration struct { +} + +func (CodecExif9101ComponentsConfiguration) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + cc, ok := value.(TagExif9101ComponentsConfiguration) + if ok == false { + log.Panicf("can only encode a TagExif9101ComponentsConfiguration") + } + + return cc.ConfigurationBytes, uint32(len(cc.ConfigurationBytes)), nil +} + +func (CodecExif9101ComponentsConfiguration) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + valueContext.SetUndefinedValueType(exifcommon.TypeByte) + + valueBytes, err := valueContext.ReadBytes() + log.PanicIf(err) + + for configurationId, configurationBytes := range TagUndefinedType_9101_ComponentsConfiguration_Configurations { + if bytes.Equal(configurationBytes, valueBytes) == true { + cc := TagExif9101ComponentsConfiguration{ + ConfigurationId: configurationId, + ConfigurationBytes: valueBytes, + } + + return cc, nil + } + } + + cc := TagExif9101ComponentsConfiguration{ + ConfigurationId: TagUndefinedType_9101_ComponentsConfiguration_OTHER, + ConfigurationBytes: valueBytes, + } + + return cc, nil +} + +func init() { + registerEncoder( + TagExif9101ComponentsConfiguration{}, + CodecExif9101ComponentsConfiguration{}) + + registerDecoder( + exifcommon.IfdExifStandardIfdIdentity.UnindexedString(), + 0x9101, + CodecExif9101ComponentsConfiguration{}) +} diff --git a/undefined/exif_9101_components_configuration_test.go b/undefined/exif_9101_components_configuration_test.go new file mode 100644 index 0000000..0136112 --- /dev/null +++ b/undefined/exif_9101_components_configuration_test.go @@ -0,0 +1,82 @@ +package exifundefined + +import ( + "bytes" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +func TestTagExif9101ComponentsConfiguration_String(t *testing.T) { + ut := TagExif9101ComponentsConfiguration{ + ConfigurationId: TagUndefinedType_9101_ComponentsConfiguration_RGB, + ConfigurationBytes: []byte{0x11, 0x22, 0x33, 0x44}, + } + + s := ut.String() + + if s != "Exif9101ComponentsConfiguration" { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestCodecExif9101ComponentsConfiguration_Encode(t *testing.T) { + configurationBytes := []byte(TagUndefinedType_9101_ComponentsConfiguration_Names[TagUndefinedType_9101_ComponentsConfiguration_RGB]) + + ut := TagExif9101ComponentsConfiguration{ + ConfigurationId: TagUndefinedType_9101_ComponentsConfiguration_RGB, + ConfigurationBytes: configurationBytes, + } + + codec := CodecExif9101ComponentsConfiguration{} + + encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder) + log.PanicIf(err) + + if bytes.Equal(encoded, configurationBytes) != true { + exifcommon.DumpBytesClause(encoded) + + t.Fatalf("Encoded bytes not correct: %v", encoded) + } else if unitCount != uint32(len(configurationBytes)) { + t.Fatalf("Unit-count not correct: (%d)", unitCount) + } + + s := string(configurationBytes) + + if s != TagUndefinedType_9101_ComponentsConfiguration_Names[TagUndefinedType_9101_ComponentsConfiguration_RGB] { + t.Fatalf("Recovered configuration name not correct: [%s]", s) + } +} + +func TestCodecExif9101ComponentsConfiguration_Decode(t *testing.T) { + configurationBytes := TagUndefinedType_9101_ComponentsConfiguration_Configurations[TagUndefinedType_9101_ComponentsConfiguration_RGB] + + ut := TagExif9101ComponentsConfiguration{ + ConfigurationId: TagUndefinedType_9101_ComponentsConfiguration_RGB, + ConfigurationBytes: configurationBytes, + } + + rawValueOffset := configurationBytes + + valueContext := exifcommon.NewValueContext( + "", + 0, + uint32(len(configurationBytes)), + 0, + rawValueOffset, + nil, + exifcommon.TypeUndefined, + exifcommon.TestDefaultByteOrder) + + codec := CodecExif9101ComponentsConfiguration{} + + value, err := codec.Decode(valueContext) + log.PanicIf(err) + + if reflect.DeepEqual(value, ut) != true { + t.Fatalf("Decoded value not correct: %s != %s\n", value, ut) + } +} diff --git a/undefined/exif_927C_maker_note.go b/undefined/exif_927C_maker_note.go new file mode 100644 index 0000000..f80a28d --- /dev/null +++ b/undefined/exif_927C_maker_note.go @@ -0,0 +1,114 @@ +package exifundefined + +import ( + "fmt" + "strings" + + "crypto/sha1" + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +type Tag927CMakerNote struct { + MakerNoteType []byte + MakerNoteBytes []byte +} + +func (Tag927CMakerNote) EncoderName() string { + return "Codec927CMakerNote" +} + +func (mn Tag927CMakerNote) String() string { + parts := make([]string, len(mn.MakerNoteType)) + + 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", strings.Join(parts, " "), len(mn.MakerNoteBytes), digest) +} + +type Codec927CMakerNote struct { +} + +func (Codec927CMakerNote) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + mn, ok := value.(Tag927CMakerNote) + if ok == false { + log.Panicf("can only encode a Tag927CMakerNote") + } + + // TODO(dustin): Confirm this size against the specification. + + return mn.MakerNoteBytes, uint32(len(mn.MakerNoteBytes)), nil +} + +func (Codec927CMakerNote) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // 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). + + valueContext.SetUndefinedValueType(exifcommon.TypeByte) + + valueBytes, err := valueContext.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) + // } + + var makerNoteType []byte + if len(valueBytes) >= 20 { + makerNoteType = valueBytes[:20] + } else { + makerNoteType = valueBytes + } + + mn := Tag927CMakerNote{ + MakerNoteType: makerNoteType, + + // 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 +} + +func init() { + registerEncoder( + Tag927CMakerNote{}, + Codec927CMakerNote{}) + + registerDecoder( + exifcommon.IfdExifStandardIfdIdentity.UnindexedString(), + 0x927c, + Codec927CMakerNote{}) +} diff --git a/undefined/exif_927C_maker_note_test.go b/undefined/exif_927C_maker_note_test.go new file mode 100644 index 0000000..0854673 --- /dev/null +++ b/undefined/exif_927C_maker_note_test.go @@ -0,0 +1,75 @@ +package exifundefined + +import ( + "bytes" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + "github.com/dsoprea/go-utility/v2/filesystem" + + "b612.me/exif/common" +) + +func TestTag927CMakerNote_String(t *testing.T) { + ut := Tag927CMakerNote{ + MakerNoteType: []byte{0, 1, 2, 3, 4}, + MakerNoteBytes: []byte{5, 6, 7, 8, 9}, + } + + s := ut.String() + if s != "MakerNote" { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestCodec927CMakerNote_Encode(t *testing.T) { + codec := Codec927CMakerNote{} + + prefix := []byte{0, 1, 2, 3, 4} + b := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + + ut := Tag927CMakerNote{ + MakerNoteType: prefix, + MakerNoteBytes: b, + } + + encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder) + log.PanicIf(err) + + if bytes.Equal(encoded, b) != true { + t.Fatalf("Encoding not correct: %v", encoded) + } else if unitCount != uint32(len(b)) { + t.Fatalf("Unit-count not correct: (%d)", len(b)) + } +} + +func TestCodec927CMakerNote_Decode(t *testing.T) { + b := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + + ut := Tag927CMakerNote{ + MakerNoteType: b, + MakerNoteBytes: b, + } + + sb := rifs.NewSeekableBufferWithBytes(b) + + valueContext := exifcommon.NewValueContext( + "", + 0, + uint32(len(b)), + 0, + nil, + sb, + exifcommon.TypeUndefined, + exifcommon.TestDefaultByteOrder) + + codec := Codec927CMakerNote{} + + value, err := codec.Decode(valueContext) + log.PanicIf(err) + + if reflect.DeepEqual(value, ut) != true { + t.Fatalf("Decoded value not correct: %s != %s", value, ut) + } +} diff --git a/undefined/exif_9286_user_comment.go b/undefined/exif_9286_user_comment.go new file mode 100644 index 0000000..2c3c8d9 --- /dev/null +++ b/undefined/exif_9286_user_comment.go @@ -0,0 +1,142 @@ +package exifundefined + +import ( + "bytes" + "fmt" + + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +var ( + exif9286Logger = log.NewLogger("exifundefined.exif_9286_user_comment") +) + +const ( + TagUndefinedType_9286_UserComment_Encoding_ASCII = iota + TagUndefinedType_9286_UserComment_Encoding_JIS = iota + TagUndefinedType_9286_UserComment_Encoding_UNICODE = iota + TagUndefinedType_9286_UserComment_Encoding_UNDEFINED = iota +) + +var ( + TagUndefinedType_9286_UserComment_Encoding_Names = map[int]string{ + TagUndefinedType_9286_UserComment_Encoding_ASCII: "ASCII", + TagUndefinedType_9286_UserComment_Encoding_JIS: "JIS", + TagUndefinedType_9286_UserComment_Encoding_UNICODE: "UNICODE", + TagUndefinedType_9286_UserComment_Encoding_UNDEFINED: "UNDEFINED", + } + + TagUndefinedType_9286_UserComment_Encodings = map[int][]byte{ + TagUndefinedType_9286_UserComment_Encoding_ASCII: {'A', 'S', 'C', 'I', 'I', 0, 0, 0}, + TagUndefinedType_9286_UserComment_Encoding_JIS: {'J', 'I', 'S', 0, 0, 0, 0, 0}, + TagUndefinedType_9286_UserComment_Encoding_UNICODE: {'U', 'n', 'i', 'c', 'o', 'd', 'e', 0}, + TagUndefinedType_9286_UserComment_Encoding_UNDEFINED: {0, 0, 0, 0, 0, 0, 0, 0}, + } +) + +type Tag9286UserComment struct { + EncodingType int + EncodingBytes []byte +} + +func (Tag9286UserComment) EncoderName() string { + return "Codec9286UserComment" +} + +func (uc Tag9286UserComment) String() string { + var valuePhrase string + + if uc.EncodingType == TagUndefinedType_9286_UserComment_Encoding_ASCII { + return fmt.Sprintf("[ASCII] %s", string(uc.EncodingBytes)) + } else { + if len(uc.EncodingBytes) <= 8 { + valuePhrase = fmt.Sprintf("%v", uc.EncodingBytes) + } else { + valuePhrase = fmt.Sprintf("%v...", uc.EncodingBytes[:8]) + } + } + + return fmt.Sprintf("UserComment", len(uc.EncodingBytes), TagUndefinedType_9286_UserComment_Encoding_Names[uc.EncodingType], valuePhrase, len(uc.EncodingBytes)) +} + +type Codec9286UserComment struct { +} + +func (Codec9286UserComment) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + uc, ok := value.(Tag9286UserComment) + if ok == false { + log.Panicf("can only encode a Tag9286UserComment") + } + + encodingTypeBytes, found := TagUndefinedType_9286_UserComment_Encodings[uc.EncodingType] + if found == false { + log.Panicf("encoding-type not valid for unknown-type tag 9286 (UserComment): (%d)", uc.EncodingType) + } + + encoded = make([]byte, len(uc.EncodingBytes)+8) + + copy(encoded[:8], encodingTypeBytes) + copy(encoded[8:], uc.EncodingBytes) + + // TODO(dustin): Confirm this size against the specification. + + return encoded, uint32(len(encoded)), nil +} + +func (Codec9286UserComment) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + valueContext.SetUndefinedValueType(exifcommon.TypeByte) + + valueBytes, err := valueContext.ReadBytes() + log.PanicIf(err) + + if len(valueBytes) < 8 { + return nil, ErrUnparseableValue + } + + unknownUc := Tag9286UserComment{ + EncodingType: TagUndefinedType_9286_UserComment_Encoding_UNDEFINED, + EncodingBytes: []byte{}, + } + + encoding := valueBytes[:8] + for encodingIndex, encodingBytes := range TagUndefinedType_9286_UserComment_Encodings { + if bytes.Compare(encoding, encodingBytes) == 0 { + uc := Tag9286UserComment{ + EncodingType: encodingIndex, + EncodingBytes: valueBytes[8:], + } + + return uc, nil + } + } + + exif9286Logger.Warningf(nil, "User-comment encoding not valid. Returning 'unknown' type (the default).") + return unknownUc, nil +} + +func init() { + registerEncoder( + Tag9286UserComment{}, + Codec9286UserComment{}) + + registerDecoder( + exifcommon.IfdExifStandardIfdIdentity.UnindexedString(), + 0x9286, + Codec9286UserComment{}) +} diff --git a/undefined/exif_9286_user_comment_test.go b/undefined/exif_9286_user_comment_test.go new file mode 100644 index 0000000..278d5ac --- /dev/null +++ b/undefined/exif_9286_user_comment_test.go @@ -0,0 +1,96 @@ +package exifundefined + +import ( + "bytes" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + "github.com/dsoprea/go-utility/v2/filesystem" + + "b612.me/exif/common" +) + +func TestTag9286UserComment_String(t *testing.T) { + comment := "some comment" + + ut := Tag9286UserComment{ + EncodingType: TagUndefinedType_9286_UserComment_Encoding_ASCII, + EncodingBytes: []byte(comment), + } + + s := ut.String() + if s != "[ASCII] some comment" { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestCodec9286UserComment_Encode(t *testing.T) { + comment := "some comment" + + ut := Tag9286UserComment{ + EncodingType: TagUndefinedType_9286_UserComment_Encoding_ASCII, + EncodingBytes: []byte(comment), + } + + codec := Codec9286UserComment{} + + encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder) + log.PanicIf(err) + + typeBytes := TagUndefinedType_9286_UserComment_Encodings[TagUndefinedType_9286_UserComment_Encoding_ASCII] + if bytes.Equal(encoded[:8], typeBytes) != true { + exifcommon.DumpBytesClause(encoded[:8]) + + t.Fatalf("Encoding type not correct.") + } + + if bytes.Equal(encoded[8:], []byte(comment)) != true { + exifcommon.DumpBytesClause(encoded[8:]) + + t.Fatalf("Encoded comment not correct.") + } + + if unitCount != uint32(len(encoded)) { + t.Fatalf("Unit-count not correct: (%d)", unitCount) + } + + exifcommon.DumpBytesClause(encoded) + +} + +func TestCodec9286UserComment_Decode(t *testing.T) { + encoded := []byte{ + 0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00, + 0x73, 0x6f, 0x6d, 0x65, 0x20, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, + } + + addressableBytes := encoded + sb := rifs.NewSeekableBufferWithBytes(addressableBytes) + + valueContext := exifcommon.NewValueContext( + "", + 0, + uint32(len(encoded)), + 0, + nil, + sb, + exifcommon.TypeUndefined, + exifcommon.TestDefaultByteOrder) + + codec := Codec9286UserComment{} + + decoded, err := codec.Decode(valueContext) + log.PanicIf(err) + + comment := "some comment" + + expectedUt := Tag9286UserComment{ + EncodingType: TagUndefinedType_9286_UserComment_Encoding_ASCII, + EncodingBytes: []byte(comment), + } + + if reflect.DeepEqual(decoded, expectedUt) != true { + t.Fatalf("Decoded struct not correct.") + } +} diff --git a/undefined/exif_A000_flashpix_version.go b/undefined/exif_A000_flashpix_version.go new file mode 100644 index 0000000..50b8fb1 --- /dev/null +++ b/undefined/exif_A000_flashpix_version.go @@ -0,0 +1,69 @@ +package exifundefined + +import ( + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +type TagA000FlashpixVersion struct { + FlashpixVersion string +} + +func (TagA000FlashpixVersion) EncoderName() string { + return "CodecA000FlashpixVersion" +} + +func (fv TagA000FlashpixVersion) String() string { + return fv.FlashpixVersion +} + +type CodecA000FlashpixVersion struct { +} + +func (CodecA000FlashpixVersion) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + s, ok := value.(TagA000FlashpixVersion) + if ok == false { + log.Panicf("can only encode a TagA000FlashpixVersion") + } + + return []byte(s.FlashpixVersion), uint32(len(s.FlashpixVersion)), nil +} + +func (CodecA000FlashpixVersion) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + valueContext.SetUndefinedValueType(exifcommon.TypeAsciiNoNul) + + valueString, err := valueContext.ReadAsciiNoNul() + log.PanicIf(err) + + fv := TagA000FlashpixVersion{ + FlashpixVersion: valueString, + } + + return fv, nil +} + +func init() { + registerEncoder( + TagA000FlashpixVersion{}, + CodecA000FlashpixVersion{}) + + registerDecoder( + exifcommon.IfdExifStandardIfdIdentity.UnindexedString(), + 0xa000, + CodecA000FlashpixVersion{}) +} diff --git a/undefined/exif_A000_flashpix_version_test.go b/undefined/exif_A000_flashpix_version_test.go new file mode 100644 index 0000000..da89275 --- /dev/null +++ b/undefined/exif_A000_flashpix_version_test.go @@ -0,0 +1,72 @@ +package exifundefined + +import ( + "bytes" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + "github.com/dsoprea/go-utility/v2/filesystem" + + "b612.me/exif/common" +) + +func TestTagA000FlashpixVersion_String(t *testing.T) { + versionPhrase := "some version" + + ut := TagA000FlashpixVersion{versionPhrase} + + s := ut.String() + if s != versionPhrase { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestCodecA000FlashpixVersion_Encode(t *testing.T) { + versionPhrase := "some version" + + ut := TagA000FlashpixVersion{versionPhrase} + + codec := CodecA000FlashpixVersion{} + + encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder) + log.PanicIf(err) + + if bytes.Equal(encoded, []byte(versionPhrase)) != true { + exifcommon.DumpBytesClause(encoded) + + t.Fatalf("Encoding not correct.") + } else if unitCount != uint32(len(encoded)) { + t.Fatalf("Unit-count not correct: (%d)", unitCount) + } +} + +func TestCodecA000FlashpixVersion_Decode(t *testing.T) { + versionPhrase := "some version" + + expectedUt := TagA000FlashpixVersion{versionPhrase} + + encoded := []byte(versionPhrase) + + addressableBytes := encoded + sb := rifs.NewSeekableBufferWithBytes(addressableBytes) + + valueContext := exifcommon.NewValueContext( + "", + 0, + uint32(len(encoded)), + 0, + nil, + sb, + exifcommon.TypeUndefined, + exifcommon.TestDefaultByteOrder) + + codec := CodecA000FlashpixVersion{} + + decoded, err := codec.Decode(valueContext) + log.PanicIf(err) + + if reflect.DeepEqual(decoded, expectedUt) != true { + t.Fatalf("Decoded struct not correct.") + } +} diff --git a/undefined/exif_A20C_spatial_frequency_response.go b/undefined/exif_A20C_spatial_frequency_response.go new file mode 100644 index 0000000..dc05407 --- /dev/null +++ b/undefined/exif_A20C_spatial_frequency_response.go @@ -0,0 +1,160 @@ +package exifundefined + +import ( + "bytes" + "fmt" + + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +type TagA20CSpatialFrequencyResponse struct { + Columns uint16 + Rows uint16 + ColumnNames []string + Values []exifcommon.Rational +} + +func (TagA20CSpatialFrequencyResponse) EncoderName() string { + return "CodecA20CSpatialFrequencyResponse" +} + +func (sfr TagA20CSpatialFrequencyResponse) String() string { + return fmt.Sprintf("CodecA20CSpatialFrequencyResponse", sfr.Columns, sfr.Rows) +} + +type CodecA20CSpatialFrequencyResponse struct { +} + +func (CodecA20CSpatialFrequencyResponse) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test. + + sfr, ok := value.(TagA20CSpatialFrequencyResponse) + if ok == false { + log.Panicf("can only encode a TagA20CSpatialFrequencyResponse") + } + + b := new(bytes.Buffer) + + err = binary.Write(b, byteOrder, sfr.Columns) + log.PanicIf(err) + + err = binary.Write(b, byteOrder, sfr.Rows) + log.PanicIf(err) + + // Write columns. + + for _, name := range sfr.ColumnNames { + _, err := b.WriteString(name) + log.PanicIf(err) + + err = b.WriteByte(0) + log.PanicIf(err) + } + + // Write values. + + ve := exifcommon.NewValueEncoder(byteOrder) + + ed, err := ve.Encode(sfr.Values) + log.PanicIf(err) + + _, err = b.Write(ed.Encoded) + log.PanicIf(err) + + encoded = b.Bytes() + + // TODO(dustin): Confirm this size against the specification. + + return encoded, uint32(len(encoded)), nil +} + +func (CodecA20CSpatialFrequencyResponse) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test using known good data. + + byteOrder := valueContext.ByteOrder() + + valueContext.SetUndefinedValueType(exifcommon.TypeByte) + + valueBytes, err := valueContext.ReadBytes() + log.PanicIf(err) + + sfr := TagA20CSpatialFrequencyResponse{} + + sfr.Columns = byteOrder.Uint16(valueBytes[0:2]) + sfr.Rows = byteOrder.Uint16(valueBytes[2:4]) + + columnNames := make([]string, sfr.Columns) + + // startAt is where the current column name starts. + startAt := 4 + + // offset is our current position. + offset := 4 + + currentColumnNumber := uint16(0) + + for currentColumnNumber < sfr.Columns { + if valueBytes[offset] == 0 { + columnName := string(valueBytes[startAt:offset]) + if len(columnName) == 0 { + log.Panicf("SFR column (%d) has zero length", currentColumnNumber) + } + + columnNames[currentColumnNumber] = columnName + currentColumnNumber++ + + offset++ + startAt = offset + continue + } + + offset++ + } + + sfr.ColumnNames = columnNames + + rawRationalBytes := valueBytes[offset:] + + rationalSize := exifcommon.TypeRational.Size() + if len(rawRationalBytes)%rationalSize > 0 { + log.Panicf("SFR rationals not aligned: (%d) %% (%d) > 0", len(rawRationalBytes), rationalSize) + } + + rationalCount := len(rawRationalBytes) / rationalSize + + parser := new(exifcommon.Parser) + + items, err := parser.ParseRationals(rawRationalBytes, uint32(rationalCount), byteOrder) + log.PanicIf(err) + + sfr.Values = items + + return sfr, nil +} + +func init() { + registerEncoder( + TagA20CSpatialFrequencyResponse{}, + CodecA20CSpatialFrequencyResponse{}) + + registerDecoder( + exifcommon.IfdExifStandardIfdIdentity.UnindexedString(), + 0xa20c, + CodecA20CSpatialFrequencyResponse{}) +} diff --git a/undefined/exif_A20C_spatial_frequency_response_test.go b/undefined/exif_A20C_spatial_frequency_response_test.go new file mode 100644 index 0000000..c01f250 --- /dev/null +++ b/undefined/exif_A20C_spatial_frequency_response_test.go @@ -0,0 +1,106 @@ +package exifundefined + +import ( + "bytes" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + "github.com/dsoprea/go-utility/v2/filesystem" + + "b612.me/exif/common" +) + +func TestTagA20CSpatialFrequencyResponse_String(t *testing.T) { + ut := TagA20CSpatialFrequencyResponse{ + Columns: 2, + Rows: 9, + ColumnNames: []string{"column1", "column2"}, + Values: []exifcommon.Rational{ + {1, 2}, + {3, 4}, + }, + } + + s := ut.String() + if s != "CodecA20CSpatialFrequencyResponse" { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestCodecA20CSpatialFrequencyResponse_Encode(t *testing.T) { + ut := TagA20CSpatialFrequencyResponse{ + Columns: 2, + Rows: 9, + ColumnNames: []string{"column1", "column2"}, + Values: []exifcommon.Rational{ + {1, 2}, + {3, 4}, + }, + } + + codec := CodecA20CSpatialFrequencyResponse{} + + encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder) + log.PanicIf(err) + + expectedEncoded := []byte{ + 0x00, 0x02, + 0x00, 0x09, + 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x31, 0x00, + 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x32, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, + } + + if bytes.Equal(encoded, expectedEncoded) != true { + exifcommon.DumpBytesClause(encoded) + + t.Fatalf("Encoding not correct.") + } else if unitCount != uint32(len(encoded)) { + t.Fatalf("Unit-count not correct: (%d)", unitCount) + } +} + +func TestCodecA20CSpatialFrequencyResponse_Decode(t *testing.T) { + expectedUt := TagA20CSpatialFrequencyResponse{ + Columns: 2, + Rows: 9, + ColumnNames: []string{"column1", "column2"}, + Values: []exifcommon.Rational{ + {1, 2}, + {3, 4}, + }, + } + + encoded := []byte{ + 0x00, 0x02, + 0x00, 0x09, + 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x31, 0x00, + 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x32, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, + } + + addressableBytes := encoded + sb := rifs.NewSeekableBufferWithBytes(addressableBytes) + + valueContext := exifcommon.NewValueContext( + "", + 0, + uint32(len(encoded)), + 0, + nil, + sb, + exifcommon.TypeUndefined, + exifcommon.TestDefaultByteOrder) + + codec := CodecA20CSpatialFrequencyResponse{} + + decoded, err := codec.Decode(valueContext) + log.PanicIf(err) + + if reflect.DeepEqual(decoded, expectedUt) != true { + t.Fatalf("Decoded struct not correct.") + } +} diff --git a/undefined/exif_A300_file_source.go b/undefined/exif_A300_file_source.go new file mode 100644 index 0000000..e84bea8 --- /dev/null +++ b/undefined/exif_A300_file_source.go @@ -0,0 +1,79 @@ +package exifundefined + +import ( + "fmt" + + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +type TagExifA300FileSource uint32 + +func (TagExifA300FileSource) EncoderName() string { + return "CodecExifA300FileSource" +} + +func (af TagExifA300FileSource) String() string { + return fmt.Sprintf("0x%08x", uint32(af)) +} + +const ( + TagUndefinedType_A300_SceneType_Others TagExifA300FileSource = 0 + TagUndefinedType_A300_SceneType_ScannerOfTransparentType TagExifA300FileSource = 1 + TagUndefinedType_A300_SceneType_ScannerOfReflexType TagExifA300FileSource = 2 + TagUndefinedType_A300_SceneType_Dsc TagExifA300FileSource = 3 +) + +type CodecExifA300FileSource struct { +} + +func (CodecExifA300FileSource) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + st, ok := value.(TagExifA300FileSource) + if ok == false { + log.Panicf("can only encode a TagExifA300FileSource") + } + + ve := exifcommon.NewValueEncoder(byteOrder) + + ed, err := ve.Encode([]uint32{uint32(st)}) + log.PanicIf(err) + + // TODO(dustin): Confirm this size against the specification. It's non-specific about what type it is, but it looks to be no more than a single integer scalar. So, we're assuming it's a LONG. + + return ed.Encoded, 1, nil +} + +func (CodecExifA300FileSource) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + valueContext.SetUndefinedValueType(exifcommon.TypeLong) + + valueLongs, err := valueContext.ReadLongs() + log.PanicIf(err) + + return TagExifA300FileSource(valueLongs[0]), nil +} + +func init() { + registerEncoder( + TagExifA300FileSource(0), + CodecExifA300FileSource{}) + + registerDecoder( + exifcommon.IfdExifStandardIfdIdentity.UnindexedString(), + 0xa300, + CodecExifA300FileSource{}) +} diff --git a/undefined/exif_A300_file_source_test.go b/undefined/exif_A300_file_source_test.go new file mode 100644 index 0000000..d601503 --- /dev/null +++ b/undefined/exif_A300_file_source_test.go @@ -0,0 +1,66 @@ +package exifundefined + +import ( + "bytes" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +func TestTagExifA300FileSource_String(t *testing.T) { + ut := TagExifA300FileSource(0x1234) + + s := ut.String() + if s != "0x00001234" { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestCodecExifA300FileSource_Encode(t *testing.T) { + ut := TagExifA300FileSource(0x1234) + + codec := CodecExifA300FileSource{} + + encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder) + log.PanicIf(err) + + expectedEncoded := []byte{0, 0, 0x12, 0x34} + + if bytes.Equal(encoded, expectedEncoded) != true { + exifcommon.DumpBytesClause(encoded) + + t.Fatalf("Encoding not correct.") + } else if unitCount != 1 { + t.Fatalf("Unit-count not correct: (%d)", unitCount) + } +} + +func TestCodecExifA300FileSource_Decode(t *testing.T) { + expectedUt := TagExifA300FileSource(0x1234) + + encoded := []byte{0, 0, 0x12, 0x34} + + rawValueOffset := encoded + + valueContext := exifcommon.NewValueContext( + "", + 0, + 1, + 0, + rawValueOffset, + nil, + exifcommon.TypeUndefined, + exifcommon.TestDefaultByteOrder) + + codec := CodecExifA300FileSource{} + + decoded, err := codec.Decode(valueContext) + log.PanicIf(err) + + if reflect.DeepEqual(decoded, expectedUt) != true { + t.Fatalf("Decoded struct not correct.") + } +} diff --git a/undefined/exif_A301_scene_type.go b/undefined/exif_A301_scene_type.go new file mode 100644 index 0000000..b670287 --- /dev/null +++ b/undefined/exif_A301_scene_type.go @@ -0,0 +1,76 @@ +package exifundefined + +import ( + "fmt" + + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +type TagExifA301SceneType uint32 + +func (TagExifA301SceneType) EncoderName() string { + return "CodecExifA301SceneType" +} + +func (st TagExifA301SceneType) String() string { + return fmt.Sprintf("0x%08x", uint32(st)) +} + +const ( + TagUndefinedType_A301_SceneType_DirectlyPhotographedImage TagExifA301SceneType = 1 +) + +type CodecExifA301SceneType struct { +} + +func (CodecExifA301SceneType) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + st, ok := value.(TagExifA301SceneType) + if ok == false { + log.Panicf("can only encode a TagExif9101ComponentsConfiguration") + } + + ve := exifcommon.NewValueEncoder(byteOrder) + + ed, err := ve.Encode([]uint32{uint32(st)}) + log.PanicIf(err) + + // TODO(dustin): Confirm this size against the specification. It's non-specific about what type it is, but it looks to be no more than a single integer scalar. So, we're assuming it's a LONG. + + return ed.Encoded, uint32(int(ed.UnitCount)), nil +} + +func (CodecExifA301SceneType) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + valueContext.SetUndefinedValueType(exifcommon.TypeLong) + + valueLongs, err := valueContext.ReadLongs() + log.PanicIf(err) + + return TagExifA301SceneType(valueLongs[0]), nil +} + +func init() { + registerEncoder( + TagExifA301SceneType(0), + CodecExifA301SceneType{}) + + registerDecoder( + exifcommon.IfdExifStandardIfdIdentity.UnindexedString(), + 0xa301, + CodecExifA301SceneType{}) +} diff --git a/undefined/exif_A301_scene_type_test.go b/undefined/exif_A301_scene_type_test.go new file mode 100644 index 0000000..ce3d73f --- /dev/null +++ b/undefined/exif_A301_scene_type_test.go @@ -0,0 +1,66 @@ +package exifundefined + +import ( + "bytes" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +func TestTagExifA301SceneType_String(t *testing.T) { + ut := TagExifA301SceneType(0x1234) + + s := ut.String() + if s != "0x00001234" { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestCodecExifA301SceneType_Encode(t *testing.T) { + ut := TagExifA301SceneType(0x1234) + + codec := CodecExifA301SceneType{} + + encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder) + log.PanicIf(err) + + expectedEncoded := []byte{0, 0, 0x12, 0x34} + + if bytes.Equal(encoded, expectedEncoded) != true { + exifcommon.DumpBytesClause(encoded) + + t.Fatalf("Encoding not correct.") + } else if unitCount != 1 { + t.Fatalf("Unit-count not correct: (%d)", unitCount) + } +} + +func TestCodecExifA301SceneType_Decode(t *testing.T) { + expectedUt := TagExifA301SceneType(0x1234) + + encoded := []byte{0, 0, 0x12, 0x34} + + rawValueOffset := encoded + + valueContext := exifcommon.NewValueContext( + "", + 0, + 1, + 0, + rawValueOffset, + nil, + exifcommon.TypeUndefined, + exifcommon.TestDefaultByteOrder) + + codec := CodecExifA301SceneType{} + + decoded, err := codec.Decode(valueContext) + log.PanicIf(err) + + if reflect.DeepEqual(decoded, expectedUt) != true { + t.Fatalf("Decoded struct not correct.") + } +} diff --git a/undefined/exif_A302_cfa_pattern.go b/undefined/exif_A302_cfa_pattern.go new file mode 100644 index 0000000..d3e40b2 --- /dev/null +++ b/undefined/exif_A302_cfa_pattern.go @@ -0,0 +1,97 @@ +package exifundefined + +import ( + "bytes" + "fmt" + + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +type TagA302CfaPattern struct { + HorizontalRepeat uint16 + VerticalRepeat uint16 + CfaValue []byte +} + +func (TagA302CfaPattern) EncoderName() string { + return "CodecA302CfaPattern" +} + +func (cp TagA302CfaPattern) String() string { + return fmt.Sprintf("TagA302CfaPattern", cp.HorizontalRepeat, cp.VerticalRepeat, len(cp.CfaValue)) +} + +type CodecA302CfaPattern struct { +} + +func (CodecA302CfaPattern) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test. + + cp, ok := value.(TagA302CfaPattern) + if ok == false { + log.Panicf("can only encode a TagA302CfaPattern") + } + + b := new(bytes.Buffer) + + err = binary.Write(b, byteOrder, cp.HorizontalRepeat) + log.PanicIf(err) + + err = binary.Write(b, byteOrder, cp.VerticalRepeat) + log.PanicIf(err) + + _, err = b.Write(cp.CfaValue) + log.PanicIf(err) + + encoded = b.Bytes() + + // TODO(dustin): Confirm this size against the specification. + + return encoded, uint32(len(encoded)), nil +} + +func (CodecA302CfaPattern) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test using known good data. + + valueContext.SetUndefinedValueType(exifcommon.TypeByte) + + valueBytes, err := valueContext.ReadBytes() + log.PanicIf(err) + + cp := TagA302CfaPattern{} + + cp.HorizontalRepeat = valueContext.ByteOrder().Uint16(valueBytes[0:2]) + cp.VerticalRepeat = valueContext.ByteOrder().Uint16(valueBytes[2:4]) + + expectedLength := int(cp.HorizontalRepeat * cp.VerticalRepeat) + cp.CfaValue = valueBytes[4 : 4+expectedLength] + + return cp, nil +} + +func init() { + registerEncoder( + TagA302CfaPattern{}, + CodecA302CfaPattern{}) + + registerDecoder( + exifcommon.IfdExifStandardIfdIdentity.UnindexedString(), + 0xa302, + CodecA302CfaPattern{}) +} diff --git a/undefined/exif_A302_cfa_pattern_test.go b/undefined/exif_A302_cfa_pattern_test.go new file mode 100644 index 0000000..34894b6 --- /dev/null +++ b/undefined/exif_A302_cfa_pattern_test.go @@ -0,0 +1,97 @@ +package exifundefined + +import ( + "bytes" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + "github.com/dsoprea/go-utility/v2/filesystem" + + "b612.me/exif/common" +) + +func TestTagA302CfaPattern_String(t *testing.T) { + ut := TagA302CfaPattern{ + HorizontalRepeat: 2, + VerticalRepeat: 3, + CfaValue: []byte{ + 0, 1, 2, 3, 4, 5, + }, + } + + s := ut.String() + + if s != "TagA302CfaPattern" { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestCodecA302CfaPattern_Encode(t *testing.T) { + ut := TagA302CfaPattern{ + HorizontalRepeat: 2, + VerticalRepeat: 3, + CfaValue: []byte{ + 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, + }, + } + + codec := CodecA302CfaPattern{} + + encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder) + log.PanicIf(err) + + expectedBytes := []byte{ + 0x00, 0x02, + 0x00, 0x03, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + } + + if bytes.Equal(encoded, expectedBytes) != true { + exifcommon.DumpBytesClause(encoded) + + t.Fatalf("Encoded bytes not correct.") + } else if unitCount != 19 { + t.Fatalf("Unit-count not correct: (%d)", unitCount) + } +} + +func TestCodecA302CfaPattern_Decode(t *testing.T) { + encoded := []byte{ + 0x00, 0x02, + 0x00, 0x03, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + } + + addressableBytes := encoded + sb := rifs.NewSeekableBufferWithBytes(addressableBytes) + + valueContext := exifcommon.NewValueContext( + "", + 0, + uint32(len(encoded)), + 0, + nil, + sb, + exifcommon.TypeUndefined, + exifcommon.TestDefaultByteOrder) + + codec := CodecA302CfaPattern{} + + value, err := codec.Decode(valueContext) + log.PanicIf(err) + + expectedValue := TagA302CfaPattern{ + HorizontalRepeat: 2, + VerticalRepeat: 3, + CfaValue: []byte{ + 0, 1, 2, 3, 4, 5, + }, + } + + if reflect.DeepEqual(value, expectedValue) != true { + t.Fatalf("Decoded value not correct: %s", value) + } +} diff --git a/undefined/exif_iop_0002_interop_version.go b/undefined/exif_iop_0002_interop_version.go new file mode 100644 index 0000000..b3baf6b --- /dev/null +++ b/undefined/exif_iop_0002_interop_version.go @@ -0,0 +1,69 @@ +package exifundefined + +import ( + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +type Tag0002InteropVersion struct { + InteropVersion string +} + +func (Tag0002InteropVersion) EncoderName() string { + return "Codec0002InteropVersion" +} + +func (iv Tag0002InteropVersion) String() string { + return iv.InteropVersion +} + +type Codec0002InteropVersion struct { +} + +func (Codec0002InteropVersion) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + s, ok := value.(Tag0002InteropVersion) + if ok == false { + log.Panicf("can only encode a Tag0002InteropVersion") + } + + return []byte(s.InteropVersion), uint32(len(s.InteropVersion)), nil +} + +func (Codec0002InteropVersion) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + valueContext.SetUndefinedValueType(exifcommon.TypeAsciiNoNul) + + valueString, err := valueContext.ReadAsciiNoNul() + log.PanicIf(err) + + iv := Tag0002InteropVersion{ + InteropVersion: valueString, + } + + return iv, nil +} + +func init() { + registerEncoder( + Tag0002InteropVersion{}, + Codec0002InteropVersion{}) + + registerDecoder( + exifcommon.IfdExifIopStandardIfdIdentity.UnindexedString(), + 0x0002, + Codec0002InteropVersion{}) +} diff --git a/undefined/exif_iop_0002_interop_version_test.go b/undefined/exif_iop_0002_interop_version_test.go new file mode 100644 index 0000000..bcf2961 --- /dev/null +++ b/undefined/exif_iop_0002_interop_version_test.go @@ -0,0 +1,64 @@ +package exifundefined + +import ( + "bytes" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +func TestTag0002InteropVersion_String(t *testing.T) { + ut := Tag0002InteropVersion{"abc"} + s := ut.String() + + if s != "abc" { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestCodec0002InteropVersion_Encode(t *testing.T) { + s := "abc" + ut := Tag0002InteropVersion{s} + + codec := Codec0002InteropVersion{} + + encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder) + log.PanicIf(err) + + if bytes.Equal(encoded, []byte(s)) != true { + t.Fatalf("Encoded bytes not correct: %v", encoded) + } else if unitCount != uint32(len(s)) { + t.Fatalf("Unit-count not correct: (%d)", unitCount) + } +} + +func TestCodec0002InteropVersion_Decode(t *testing.T) { + s := "abc" + ut := Tag0002InteropVersion{s} + + encoded := []byte(s) + + rawValueOffset := encoded + + valueContext := exifcommon.NewValueContext( + "", + 0, + uint32(len(encoded)), + 0, + rawValueOffset, + nil, + exifcommon.TypeUndefined, + exifcommon.TestDefaultByteOrder) + + codec := Codec0002InteropVersion{} + + value, err := codec.Decode(valueContext) + log.PanicIf(err) + + if reflect.DeepEqual(value, ut) != true { + t.Fatalf("Decoded value not correct: %s\n", value) + } +} diff --git a/undefined/gps_001B_gps_processing_method.go b/undefined/gps_001B_gps_processing_method.go new file mode 100644 index 0000000..db679f5 --- /dev/null +++ b/undefined/gps_001B_gps_processing_method.go @@ -0,0 +1,65 @@ +package exifundefined + +import ( + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +type Tag001BGPSProcessingMethod struct { + string +} + +func (Tag001BGPSProcessingMethod) EncoderName() string { + return "Codec001BGPSProcessingMethod" +} + +func (gpm Tag001BGPSProcessingMethod) String() string { + return gpm.string +} + +type Codec001BGPSProcessingMethod struct { +} + +func (Codec001BGPSProcessingMethod) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + s, ok := value.(Tag001BGPSProcessingMethod) + if ok == false { + log.Panicf("can only encode a Tag001BGPSProcessingMethod") + } + + return []byte(s.string), uint32(len(s.string)), nil +} + +func (Codec001BGPSProcessingMethod) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + valueContext.SetUndefinedValueType(exifcommon.TypeAsciiNoNul) + + valueString, err := valueContext.ReadAsciiNoNul() + log.PanicIf(err) + + return Tag001BGPSProcessingMethod{valueString}, nil +} + +func init() { + registerEncoder( + Tag001BGPSProcessingMethod{}, + Codec001BGPSProcessingMethod{}) + + registerDecoder( + exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString(), + 0x001b, + Codec001BGPSProcessingMethod{}) +} diff --git a/undefined/gps_001B_gps_processing_method_test.go b/undefined/gps_001B_gps_processing_method_test.go new file mode 100644 index 0000000..c1d0ace --- /dev/null +++ b/undefined/gps_001B_gps_processing_method_test.go @@ -0,0 +1,64 @@ +package exifundefined + +import ( + "bytes" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +func TestTag001BGPSProcessingMethod_String(t *testing.T) { + ut := Tag001BGPSProcessingMethod{"abc"} + s := ut.String() + + if s != "abc" { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestCodec001BGPSProcessingMethod_Encode(t *testing.T) { + s := "abc" + ut := Tag001BGPSProcessingMethod{s} + + codec := Codec001BGPSProcessingMethod{} + + encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder) + log.PanicIf(err) + + if bytes.Equal(encoded, []byte(s)) != true { + t.Fatalf("Encoded bytes not correct: %v", encoded) + } else if unitCount != uint32(len(s)) { + t.Fatalf("Unit-count not correct: (%d)", unitCount) + } +} + +func TestCodec001BGPSProcessingMethod_Decode(t *testing.T) { + s := "abc" + ut := Tag001BGPSProcessingMethod{s} + + encoded := []byte(s) + + rawValueOffset := encoded + + valueContext := exifcommon.NewValueContext( + "", + 0, + uint32(len(encoded)), + 0, + rawValueOffset, + nil, + exifcommon.TypeUndefined, + exifcommon.TestDefaultByteOrder) + + codec := Codec001BGPSProcessingMethod{} + + value, err := codec.Decode(valueContext) + log.PanicIf(err) + + if reflect.DeepEqual(value, ut) != true { + t.Fatalf("Decoded value not correct: %s\n", value) + } +} diff --git a/undefined/gps_001C_gps_area_information.go b/undefined/gps_001C_gps_area_information.go new file mode 100644 index 0000000..f29853c --- /dev/null +++ b/undefined/gps_001C_gps_area_information.go @@ -0,0 +1,65 @@ +package exifundefined + +import ( + "encoding/binary" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +type Tag001CGPSAreaInformation struct { + string +} + +func (Tag001CGPSAreaInformation) EncoderName() string { + return "Codec001CGPSAreaInformation" +} + +func (gai Tag001CGPSAreaInformation) String() string { + return gai.string +} + +type Codec001CGPSAreaInformation struct { +} + +func (Codec001CGPSAreaInformation) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + s, ok := value.(Tag001CGPSAreaInformation) + if ok == false { + log.Panicf("can only encode a Tag001CGPSAreaInformation") + } + + return []byte(s.string), uint32(len(s.string)), nil +} + +func (Codec001CGPSAreaInformation) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + valueContext.SetUndefinedValueType(exifcommon.TypeAsciiNoNul) + + valueString, err := valueContext.ReadAsciiNoNul() + log.PanicIf(err) + + return Tag001CGPSAreaInformation{valueString}, nil +} + +func init() { + registerEncoder( + Tag001CGPSAreaInformation{}, + Codec001CGPSAreaInformation{}) + + registerDecoder( + exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString(), + 0x001c, + Codec001CGPSAreaInformation{}) +} diff --git a/undefined/gps_001C_gps_area_information_test.go b/undefined/gps_001C_gps_area_information_test.go new file mode 100644 index 0000000..004cccf --- /dev/null +++ b/undefined/gps_001C_gps_area_information_test.go @@ -0,0 +1,64 @@ +package exifundefined + +import ( + "bytes" + "reflect" + "testing" + + "github.com/dsoprea/go-logging" + + "b612.me/exif/common" +) + +func TestTag001CGPSAreaInformation_String(t *testing.T) { + ut := Tag001CGPSAreaInformation{"abc"} + s := ut.String() + + if s != "abc" { + t.Fatalf("String not correct: [%s]", s) + } +} + +func TestCodec001CGPSAreaInformation_Encode(t *testing.T) { + s := "abc" + ut := Tag001CGPSAreaInformation{s} + + codec := Codec001CGPSAreaInformation{} + + encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder) + log.PanicIf(err) + + if bytes.Equal(encoded, []byte(s)) != true { + t.Fatalf("Encoded bytes not correct: %v", encoded) + } else if unitCount != uint32(len(s)) { + t.Fatalf("Unit-count not correct: (%d)", unitCount) + } +} + +func TestCodec001CGPSAreaInformation_Decode(t *testing.T) { + s := "abc" + ut := Tag001CGPSAreaInformation{s} + + encoded := []byte(s) + + rawValueOffset := encoded + + valueContext := exifcommon.NewValueContext( + "", + 0, + uint32(len(encoded)), + 0, + rawValueOffset, + nil, + exifcommon.TypeUndefined, + exifcommon.TestDefaultByteOrder) + + codec := Codec001CGPSAreaInformation{} + + value, err := codec.Decode(valueContext) + log.PanicIf(err) + + if reflect.DeepEqual(value, ut) != true { + t.Fatalf("Decoded value not correct: %s\n", value) + } +} diff --git a/undefined/registration.go b/undefined/registration.go new file mode 100644 index 0000000..cccc20a --- /dev/null +++ b/undefined/registration.go @@ -0,0 +1,42 @@ +package exifundefined + +import ( + "github.com/dsoprea/go-logging" +) + +// UndefinedTagHandle defines one undefined-type tag with a corresponding +// decoder. +type UndefinedTagHandle struct { + IfdPath string + TagId uint16 +} + +func registerEncoder(entity EncodeableValue, encoder UndefinedValueEncoder) { + typeName := entity.EncoderName() + + _, found := encoders[typeName] + if found == true { + log.Panicf("encoder already registered: %v", typeName) + } + + encoders[typeName] = encoder +} + +func registerDecoder(ifdPath string, tagId uint16, decoder UndefinedValueDecoder) { + uth := UndefinedTagHandle{ + IfdPath: ifdPath, + TagId: tagId, + } + + _, found := decoders[uth] + if found == true { + log.Panicf("decoder already registered: %v", uth) + } + + decoders[uth] = decoder +} + +var ( + encoders = make(map[string]UndefinedValueEncoder) + decoders = make(map[UndefinedTagHandle]UndefinedValueDecoder) +) diff --git a/undefined/type.go b/undefined/type.go new file mode 100644 index 0000000..31cb47f --- /dev/null +++ b/undefined/type.go @@ -0,0 +1,44 @@ +package exifundefined + +import ( + "errors" + + "encoding/binary" + + "b612.me/exif/common" +) + +const ( + // UnparseableUnknownTagValuePlaceholder is the string to use for an unknown + // undefined tag. + UnparseableUnknownTagValuePlaceholder = "!UNKNOWN" + + // UnparseableHandledTagValuePlaceholder is the string to use for a known + // value that is not parseable. + UnparseableHandledTagValuePlaceholder = "!MALFORMED" +) + +var ( + // ErrUnparseableValue is the error for a value that we should have been + // able to parse but were not able to. + ErrUnparseableValue = errors.New("unparseable undefined tag") +) + +// UndefinedValueEncoder knows how to encode an undefined-type tag's value to +// bytes. +type UndefinedValueEncoder interface { + Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) +} + +// EncodeableValue wraps a value with the information that will be needed to re- +// encode it later. +type EncodeableValue interface { + EncoderName() string + String() string +} + +// UndefinedValueDecoder knows how to decode an undefined-type tag's value from +// bytes. +type UndefinedValueDecoder interface { + Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) +} diff --git a/utility.go b/utility.go new file mode 100644 index 0000000..250ad93 --- /dev/null +++ b/utility.go @@ -0,0 +1,237 @@ +package exif + +import ( + "fmt" + "io" + "math" + + "github.com/dsoprea/go-logging" + "github.com/dsoprea/go-utility/v2/filesystem" + + "b612.me/exif/common" + "b612.me/exif/undefined" +) + +var ( + utilityLogger = log.NewLogger("exif.utility") +) + +// ExifTag is one simple representation of a tag in a flat list of all of them. +type ExifTag struct { + // IfdPath is the fully-qualified IFD path (even though it is not named as + // such). + IfdPath string `json:"ifd_path"` + + // TagId is the tag-ID. + TagId uint16 `json:"id"` + + // TagName is the tag-name. This is never empty. + TagName string `json:"name"` + + // UnitCount is the recorded number of units constution of the value. + UnitCount uint32 `json:"unit_count"` + + // TagTypeId is the type-ID. + TagTypeId exifcommon.TagTypePrimitive `json:"type_id"` + + // TagTypeName is the type name. + TagTypeName string `json:"type_name"` + + // Value is the decoded value. + Value interface{} `json:"value"` + + // ValueBytes is the raw, encoded value. + ValueBytes []byte `json:"value_bytes"` + + // Formatted is the human representation of the first value (tag values are + // always an array). + FormattedFirst string `json:"formatted_first"` + + // Formatted is the human representation of the complete value. + Formatted string `json:"formatted"` + + // ChildIfdPath is the name of the child IFD this tag represents (if it + // represents any). Otherwise, this is empty. + 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.FormattedFirst, + len(et.ValueBytes), et.ChildIfdPath) +} + +// GetFlatExifData returns a simple, flat representation of all tags. +func GetFlatExifData(exifData []byte, so *ScanOptions) (exifTags []ExifTag, med *MiscellaneousExifData, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + sb := rifs.NewSeekableBufferWithBytes(exifData) + + exifTags, med, err = getFlatExifDataUniversalSearchWithReadSeeker(sb, so, false) + log.PanicIf(err) + + return exifTags, med, nil +} + +// RELEASE(dustin): GetFlatExifDataUniversalSearch is a kludge to allow univeral tag searching in a backwards-compatible manner. For the next release, undo this and simply add the flag to GetFlatExifData. + +// GetFlatExifDataUniversalSearch returns a simple, flat representation of all +// tags. +func GetFlatExifDataUniversalSearch(exifData []byte, so *ScanOptions, doUniversalSearch bool) (exifTags []ExifTag, med *MiscellaneousExifData, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + sb := rifs.NewSeekableBufferWithBytes(exifData) + + exifTags, med, err = getFlatExifDataUniversalSearchWithReadSeeker(sb, so, doUniversalSearch) + log.PanicIf(err) + + return exifTags, med, nil +} + +// RELEASE(dustin): GetFlatExifDataUniversalSearchWithReadSeeker is a kludge to allow using a ReadSeeker in a backwards-compatible manner. For the next release, drop this and refactor GetFlatExifDataUniversalSearch to take a ReadSeeker. + +// GetFlatExifDataUniversalSearchWithReadSeeker returns a simple, flat +// representation of all tags given a ReadSeeker. +func GetFlatExifDataUniversalSearchWithReadSeeker(rs io.ReadSeeker, so *ScanOptions, doUniversalSearch bool) (exifTags []ExifTag, med *MiscellaneousExifData, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + exifTags, med, err = getFlatExifDataUniversalSearchWithReadSeeker(rs, so, doUniversalSearch) + log.PanicIf(err) + + return exifTags, med, nil +} + +// getFlatExifDataUniversalSearchWithReadSeeker returns a simple, flat +// representation of all tags given a ReadSeeker. +func getFlatExifDataUniversalSearchWithReadSeeker(rs io.ReadSeeker, so *ScanOptions, doUniversalSearch bool) (exifTags []ExifTag, med *MiscellaneousExifData, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + headerData := make([]byte, ExifSignatureLength) + if _, err = io.ReadFull(rs, headerData); err != nil { + if err == io.EOF { + return nil, nil, err + } + + log.Panic(err) + } + + eh, err := ParseExifHeader(headerData) + log.PanicIf(err) + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := NewTagIndex() + + if doUniversalSearch == true { + ti.SetUniversalSearch(true) + } + + ebs := NewExifReadSeeker(rs) + ie := NewIfdEnumerate(im, ti, ebs, eh.ByteOrder) + + exifTags = make([]ExifTag, 0) + + visitor := func(ite *IfdTagEntry) (err error) { + // This encodes down to base64. Since this an example tool and we do not + // expect to ever decode the output, we are not worried about + // specifically base64-encoding it in order to have a measure of + // control. + valueBytes, err := ite.GetRawBytes() + if err != nil { + if err == exifundefined.ErrUnparseableValue { + return nil + } + + log.Panic(err) + } + + value, err := ite.Value() + if err != nil { + if err == exifcommon.ErrUnhandledUndefinedTypedTag { + value = exifundefined.UnparseableUnknownTagValuePlaceholder + } else if log.Is(err, exifcommon.ErrParseFail) == true { + utilityLogger.Warningf(nil, + "Could not parse value for tag [%s] (%04x) [%s].", + ite.IfdPath(), ite.TagId(), ite.TagName()) + + return nil + } else { + log.Panic(err) + } + } + + et := ExifTag{ + IfdPath: ite.IfdPath(), + TagId: ite.TagId(), + TagName: ite.TagName(), + UnitCount: ite.UnitCount(), + TagTypeId: ite.TagType(), + TagTypeName: ite.TagType().String(), + Value: value, + ValueBytes: valueBytes, + ChildIfdPath: ite.ChildIfdPath(), + } + + et.Formatted, err = ite.Format() + log.PanicIf(err) + + et.FormattedFirst, err = ite.FormatFirst() + log.PanicIf(err) + + exifTags = append(exifTags, et) + + return nil + } + + med, err = ie.Scan(exifcommon.IfdStandardIfdIdentity, eh.FirstIfdOffset, visitor, nil) + log.PanicIf(err) + + return exifTags, med, nil +} + +// GpsDegreesEquals returns true if the two `GpsDegrees` are identical. +func GpsDegreesEquals(gi1, gi2 GpsDegrees) bool { + if gi2.Orientation != gi1.Orientation { + return false + } + + degreesRightBound := math.Nextafter(gi1.Degrees, gi1.Degrees+1) + minutesRightBound := math.Nextafter(gi1.Minutes, gi1.Minutes+1) + secondsRightBound := math.Nextafter(gi1.Seconds, gi1.Seconds+1) + + if gi2.Degrees < gi1.Degrees || gi2.Degrees >= degreesRightBound { + return false + } else if gi2.Minutes < gi1.Minutes || gi2.Minutes >= minutesRightBound { + return false + } else if gi2.Seconds < gi1.Seconds || gi2.Seconds >= secondsRightBound { + return false + } + + return true +} diff --git a/utility_test.go b/utility_test.go new file mode 100644 index 0000000..ad163e1 --- /dev/null +++ b/utility_test.go @@ -0,0 +1,100 @@ +package exif + +import ( + "os" + "testing" + + "github.com/dsoprea/go-logging/v2" + "github.com/dsoprea/go-utility/v2/filesystem" +) + +func TestGpsDegreesEquals_Equals(t *testing.T) { + gi := GpsDegrees{ + Orientation: 'A', + Degrees: 11.0, + Minutes: 22.0, + Seconds: 33.0, + } + + r := GpsDegreesEquals(gi, gi) + if r != true { + t.Fatalf("GpsDegrees structs were not equal as expected.") + } +} + +func TestGpsDegreesEquals_NotEqual_Orientation(t *testing.T) { + gi1 := GpsDegrees{ + Orientation: 'A', + Degrees: 11.0, + Minutes: 22.0, + Seconds: 33.0, + } + + gi2 := gi1 + gi2.Orientation = 'B' + + r := GpsDegreesEquals(gi1, gi2) + if r != false { + t.Fatalf("GpsDegrees structs were equal but not supposed to be.") + } +} + +func TestGpsDegreesEquals_NotEqual_Position(t *testing.T) { + gi1 := GpsDegrees{ + Orientation: 'A', + Degrees: 11.0, + Minutes: 22.0, + Seconds: 33.0, + } + + gi2 := gi1 + gi2.Minutes = 22.5 + + r := GpsDegreesEquals(gi1, gi2) + if r != false { + t.Fatalf("GpsDegrees structs were equal but not supposed to be.") + } +} + +func TestGetFlatExifData(t *testing.T) { + testExifData := getTestExifData() + + exifTags, _, err := GetFlatExifData(testExifData, nil) + log.PanicIf(err) + + if len(exifTags) != 59 { + t.Fatalf("Tag count not correct: (%d)", len(exifTags)) + } +} + +func TestGetFlatExifDataUniversalSearch(t *testing.T) { + testExifData := getTestExifData() + + exifTags, _, err := GetFlatExifDataUniversalSearch(testExifData, nil, false) + log.PanicIf(err) + + if len(exifTags) != 59 { + t.Fatalf("Tag count not correct: (%d)", len(exifTags)) + } +} + +func TestGetFlatExifDataUniversalSearchWithReadSeeker(t *testing.T) { + testImageFilepath := getTestImageFilepath() + + f, err := os.Open(testImageFilepath) + log.PanicIf(err) + + defer f.Close() + + rawExif, err := SearchAndExtractExifWithReader(f) + log.PanicIf(err) + + sb := rifs.NewSeekableBufferWithBytes(rawExif) + + exifTags, _, err := GetFlatExifDataUniversalSearchWithReadSeeker(sb, nil, false) + log.PanicIf(err) + + if len(exifTags) != 59 { + t.Fatalf("Tag count not correct: (%d)", len(exifTags)) + } +}