use exif-terminator

This commit is contained in:
tsmethurst 2022-01-23 14:41:31 +01:00
parent 589bb9df02
commit 7d024ce74d
117 changed files with 3873 additions and 8725 deletions

View file

@ -202,7 +202,7 @@ The following libraries and frameworks are used by GoToSocial, with gratitude
- [spf13/pflag](https://github.com/spf13/pflag); command-line flag utilities. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html). - [spf13/pflag](https://github.com/spf13/pflag); command-line flag utilities. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html).
- [spf13/viper](https://github.com/spf13/viper); configuration management. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html). - [spf13/viper](https://github.com/spf13/viper); configuration management. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html).
- [stretchr/testify](https://github.com/stretchr/testify); test framework. [MIT License](https://spdx.org/licenses/MIT.html). - [stretchr/testify](https://github.com/stretchr/testify); test framework. [MIT License](https://spdx.org/licenses/MIT.html).
- [superseriousbusiness/exifremove](https://github.com/superseriousbusiness/exifremove) forked from [scottleedavis/go-exif-remove](https://github.com/scottleedavis/go-exif-remove); EXIF data removal. [MIT License](https://spdx.org/licenses/MIT.html). - [superseriousbusiness/exif-terminator](https://github.com/superseriousbusiness/exif-terminator); EXIF data removal. [GNU AGPL v3 LICENSE](https://spdx.org/licenses/AGPL-3.0-or-later.html).
- [superseriousbusiness/activity](https://github.com/superseriousbusiness/activity) forked from [go-fed/activity](https://github.com/go-fed/activity); Golang ActivityPub/ActivityStreams library. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html). - [superseriousbusiness/activity](https://github.com/superseriousbusiness/activity) forked from [go-fed/activity](https://github.com/go-fed/activity); Golang ActivityPub/ActivityStreams library. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
- [superseriousbusiness/oauth2](https://github.com/superseriousbusiness/oauth2) forked from [go-oauth2/oauth2](https://github.com/go-oauth2/oauth2); oauth server framework and token handling. [MIT License](https://spdx.org/licenses/MIT.html). - [superseriousbusiness/oauth2](https://github.com/superseriousbusiness/oauth2) forked from [go-oauth2/oauth2](https://github.com/go-oauth2/oauth2); oauth server framework and token handling. [MIT License](https://spdx.org/licenses/MIT.html).
- [go-swagger/go-swagger](https://github.com/go-swagger/go-swagger); Swagger OpenAPI spec generation. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html). - [go-swagger/go-swagger](https://github.com/go-swagger/go-swagger); Swagger OpenAPI spec generation. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html).

12
go.mod
View file

@ -29,7 +29,7 @@ require (
github.com/spf13/viper v1.10.0 github.com/spf13/viper v1.10.0
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/superseriousbusiness/activity v1.0.1-0.20211113133524-56560b73ace8 github.com/superseriousbusiness/activity v1.0.1-0.20211113133524-56560b73ace8
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/superseriousbusiness/exif-terminator v0.1.0
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB
github.com/tdewolff/minify/v2 v2.9.22 github.com/tdewolff/minify/v2 v2.9.22
github.com/uptrace/bun v1.0.19 github.com/uptrace/bun v1.0.19
@ -50,20 +50,18 @@ require (
codeberg.org/gruf/go-fastpath v1.0.2 // indirect codeberg.org/gruf/go-fastpath v1.0.2 // indirect
codeberg.org/gruf/go-format v1.0.3 // indirect codeberg.org/gruf/go-format v1.0.3 // indirect
codeberg.org/gruf/go-hashenc v1.0.1 // indirect codeberg.org/gruf/go-hashenc v1.0.1 // indirect
codeberg.org/gruf/go-logger v1.3.2 // indirect
codeberg.org/gruf/go-mutexes v1.0.1 // indirect codeberg.org/gruf/go-mutexes v1.0.1 // indirect
codeberg.org/gruf/go-nowish v1.1.0 // indirect codeberg.org/gruf/go-nowish v1.1.0 // indirect
codeberg.org/gruf/go-pools v1.0.2 // indirect codeberg.org/gruf/go-pools v1.0.2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dsoprea/go-exif v0.0.0-20210625224831-a6301f85c82b // indirect github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b // indirect
github.com/dsoprea/go-exif/v2 v2.0.0-20210625224831-a6301f85c82b // indirect
github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 // indirect github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 // indirect
github.com/dsoprea/go-jpeg-image-structure v0.0.0-20210512043942-b434301c6836 // indirect github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20210512043942-b434301c6836 // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d // indirect github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d // indirect
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d // indirect
github.com/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e // indirect github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-errors/errors v1.4.1 // indirect github.com/go-errors/errors v1.4.1 // indirect

40
go.sum
View file

@ -51,8 +51,6 @@ codeberg.org/gruf/go-bytes v1.0.1/go.mod h1:1v/ibfaosfXSZtRdW2rWaVrDXMc9E3bsi/M9
codeberg.org/gruf/go-bytes v1.0.2 h1:malqE42Ni+h1nnYWBUAJaDDtEzF4aeN4uPN8DfMNNvo= codeberg.org/gruf/go-bytes v1.0.2 h1:malqE42Ni+h1nnYWBUAJaDDtEzF4aeN4uPN8DfMNNvo=
codeberg.org/gruf/go-bytes v1.0.2/go.mod h1:1v/ibfaosfXSZtRdW2rWaVrDXMc9E3bsi/M9Ekx39cg= codeberg.org/gruf/go-bytes v1.0.2/go.mod h1:1v/ibfaosfXSZtRdW2rWaVrDXMc9E3bsi/M9Ekx39cg=
codeberg.org/gruf/go-cache v1.1.2/go.mod h1:/Dbc+xU72Op3hMn6x2PXF3NE9uIDFeS+sXPF00hN/7o= codeberg.org/gruf/go-cache v1.1.2/go.mod h1:/Dbc+xU72Op3hMn6x2PXF3NE9uIDFeS+sXPF00hN/7o=
codeberg.org/gruf/go-errors v1.0.4 h1:jOJCn/GMb6ELLRVlnmpimGRC2CbTreH5/CBZNWh9GZA=
codeberg.org/gruf/go-errors v1.0.4/go.mod h1:rJ08LdIE79Jg8vZ2TGylz/I+tZ1UuMJkGK5mNambIfQ=
codeberg.org/gruf/go-errors v1.0.5 h1:rxV70oQkfasUdggLHxOX2QAoJOMFM7XWxHQR45Zx/Fg= codeberg.org/gruf/go-errors v1.0.5 h1:rxV70oQkfasUdggLHxOX2QAoJOMFM7XWxHQR45Zx/Fg=
codeberg.org/gruf/go-errors v1.0.5/go.mod h1:n03EpmvcmfzU3/xJKC0XXtleXXJUNFpT2fgISODvZ1Y= codeberg.org/gruf/go-errors v1.0.5/go.mod h1:n03EpmvcmfzU3/xJKC0XXtleXXJUNFpT2fgISODvZ1Y=
codeberg.org/gruf/go-fastpath v1.0.1/go.mod h1:edveE/Kp3Eqi0JJm0lXYdkVrB28cNUkcb/bRGFTPqeI= codeberg.org/gruf/go-fastpath v1.0.1/go.mod h1:edveE/Kp3Eqi0JJm0lXYdkVrB28cNUkcb/bRGFTPqeI=
@ -62,13 +60,9 @@ codeberg.org/gruf/go-format v1.0.3 h1:WoUGzTwZe6SIhILNvtr0qNIA7BOOCgdBlk5bUrfeii
codeberg.org/gruf/go-format v1.0.3/go.mod h1:k3TLXp1dqAXdDqxlon0yEM+3FFHdNn0D6BVJTwTy5As= codeberg.org/gruf/go-format v1.0.3/go.mod h1:k3TLXp1dqAXdDqxlon0yEM+3FFHdNn0D6BVJTwTy5As=
codeberg.org/gruf/go-hashenc v1.0.1 h1:EBvNe2wW8IPMUqT1XihB6/IM6KMJDLMFBxIUvmsy1f8= codeberg.org/gruf/go-hashenc v1.0.1 h1:EBvNe2wW8IPMUqT1XihB6/IM6KMJDLMFBxIUvmsy1f8=
codeberg.org/gruf/go-hashenc v1.0.1/go.mod h1:IfHhPCVScOiYmJLqdCQT9bYVS1nxNTV4ewMUvFWDPtc= codeberg.org/gruf/go-hashenc v1.0.1/go.mod h1:IfHhPCVScOiYmJLqdCQT9bYVS1nxNTV4ewMUvFWDPtc=
codeberg.org/gruf/go-logger v1.3.1/go.mod h1:tBduUc+Yb9vqGRxY9/FB0ZlYznSteLy/KmIANo7zFjA=
codeberg.org/gruf/go-logger v1.3.2 h1:/2Cg8Tmu6H10lljq/BvHE+76O2d4tDNUDwitN6YUxxk=
codeberg.org/gruf/go-logger v1.3.2/go.mod h1:q4xmTSdaxPzfndSXVF1X2xcyCVk7Nd/PIWCDs/4biMg=
codeberg.org/gruf/go-mutexes v1.0.1 h1:X9bZW74YSEplWWdCrVXAvue5ztw3w5hh+INdXTENu88= codeberg.org/gruf/go-mutexes v1.0.1 h1:X9bZW74YSEplWWdCrVXAvue5ztw3w5hh+INdXTENu88=
codeberg.org/gruf/go-mutexes v1.0.1/go.mod h1:y2hbGLkWVHhNyxBOIVsA3/y2QMm6RSrYsC3sLVZ4EXM= codeberg.org/gruf/go-mutexes v1.0.1/go.mod h1:y2hbGLkWVHhNyxBOIVsA3/y2QMm6RSrYsC3sLVZ4EXM=
codeberg.org/gruf/go-nowish v1.0.0/go.mod h1:70nvICNcqQ9OHpF07N614Dyk7cpL5ToWU1K1ZVCec2s= codeberg.org/gruf/go-nowish v1.0.0/go.mod h1:70nvICNcqQ9OHpF07N614Dyk7cpL5ToWU1K1ZVCec2s=
codeberg.org/gruf/go-nowish v1.0.2/go.mod h1:70nvICNcqQ9OHpF07N614Dyk7cpL5ToWU1K1ZVCec2s=
codeberg.org/gruf/go-nowish v1.1.0 h1:rj1z0AXDhLvnxs/DazWFxYAugs6rv5vhgWJkRCgrESg= codeberg.org/gruf/go-nowish v1.1.0 h1:rj1z0AXDhLvnxs/DazWFxYAugs6rv5vhgWJkRCgrESg=
codeberg.org/gruf/go-nowish v1.1.0/go.mod h1:70nvICNcqQ9OHpF07N614Dyk7cpL5ToWU1K1ZVCec2s= codeberg.org/gruf/go-nowish v1.1.0/go.mod h1:70nvICNcqQ9OHpF07N614Dyk7cpL5ToWU1K1ZVCec2s=
codeberg.org/gruf/go-pools v1.0.2 h1:B0X6yoCL9FVmnvyoizb1SYRwMYPWwEJBjPnBMM5ILos= codeberg.org/gruf/go-pools v1.0.2 h1:B0X6yoCL9FVmnvyoizb1SYRwMYPWwEJBjPnBMM5ILos=
@ -76,8 +70,6 @@ codeberg.org/gruf/go-pools v1.0.2/go.mod h1:MjUV3H6IASyBeBPCyCr7wjPpSNu8E2N87LG4
codeberg.org/gruf/go-runners v1.1.1/go.mod h1:9gTrmMnO3d+50C+hVzcmGBf+zTuswReS278E2EMvnmw= codeberg.org/gruf/go-runners v1.1.1/go.mod h1:9gTrmMnO3d+50C+hVzcmGBf+zTuswReS278E2EMvnmw=
codeberg.org/gruf/go-runners v1.2.0 h1:tkoPrwYMkVg1o/C4PGTR1YbC11XX4r06uLPOYajBsH4= codeberg.org/gruf/go-runners v1.2.0 h1:tkoPrwYMkVg1o/C4PGTR1YbC11XX4r06uLPOYajBsH4=
codeberg.org/gruf/go-runners v1.2.0/go.mod h1:9gTrmMnO3d+50C+hVzcmGBf+zTuswReS278E2EMvnmw= codeberg.org/gruf/go-runners v1.2.0/go.mod h1:9gTrmMnO3d+50C+hVzcmGBf+zTuswReS278E2EMvnmw=
codeberg.org/gruf/go-store v1.1.5 h1:fp28vzGD15OsAF51CCwi7woH+Y3vb0aMl4OFh9JSjA0=
codeberg.org/gruf/go-store v1.1.5/go.mod h1:Q6ev500ddKghDQ8KS4IstL/W9fptDKa2T9oeHP+tXsI=
codeberg.org/gruf/go-store v1.2.2 h1:YJPzJpZv/D3t9hQC00/u76eQDScQw4++OWjfobnjHAA= codeberg.org/gruf/go-store v1.2.2 h1:YJPzJpZv/D3t9hQC00/u76eQDScQw4++OWjfobnjHAA=
codeberg.org/gruf/go-store v1.2.2/go.mod h1:Xjw1U098th0yXF2CCx6jThQ+9FIPWAX9OGjYslO+UtE= codeberg.org/gruf/go-store v1.2.2/go.mod h1:Xjw1U098th0yXF2CCx6jThQ+9FIPWAX9OGjYslO+UtE=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@ -154,21 +146,16 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE= github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE=
github.com/dsoprea/go-exif v0.0.0-20210131231135-d154f10435cc/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs=
github.com/dsoprea/go-exif v0.0.0-20210625224831-a6301f85c82b h1:hoVHc4m/v8Al8mbWyvKJWr4Z37yM4QUSVh/NY6A5Sbc=
github.com/dsoprea/go-exif v0.0.0-20210625224831-a6301f85c82b/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E= github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
github.com/dsoprea/go-exif/v2 v2.0.0-20210625224831-a6301f85c82b h1:8lVRnnni9zebcpjkrEXrEyxFpRWG/oTpWc2Y3giKomE=
github.com/dsoprea/go-exif/v2 v2.0.0-20210625224831-a6301f85c82b/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc=
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-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk= github.com/dsoprea/go-exif/v3 v3.0.0-20210428042052-dca55bf8ca15/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b h1:NgNuLvW/gAFKU30ULWW0gtkCt56JfB7FrZ2zyo0wT8I=
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM=
github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 h1:YDRiMEm32T60Kpm35YzOK9ZHgjsS1Qrid+XskNcsdp8= github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 h1:YDRiMEm32T60Kpm35YzOK9ZHgjsS1Qrid+XskNcsdp8=
github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM=
github.com/dsoprea/go-jpeg-image-structure v0.0.0-20210128210355-86b1014917f2/go.mod h1:ZoOP3yUG0HD1T4IUjIFsz/2OAB2yB4YX6NSm4K+uJRg= github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20210512043942-b434301c6836 h1:KGCiMMWxODEMmI3+9Ms04l73efoqFVNKKKPbVyOvKrU=
github.com/dsoprea/go-jpeg-image-structure v0.0.0-20210512043942-b434301c6836 h1:OHRfKIVRz2XrhZ6A7fJKHLoKky1giN+VUgU2npF0BvE= github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20210512043942-b434301c6836/go.mod h1:WaARaUjQuSuDCDFAiU/GwzfxMTJBulfEhqEA2Tx6B4Y=
github.com/dsoprea/go-jpeg-image-structure v0.0.0-20210512043942-b434301c6836/go.mod h1:6+tQXZ+I62x13UZ+hemLVoZIuq/usVzvau7bqwUo9P0=
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-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-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 h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=
@ -176,13 +163,11 @@ github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3P
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E=
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d h1:dg6UMHa50VI01WuPWXPbNJpO8QSyvIF5T5n2IZiqX3A= github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d h1:dg6UMHa50VI01WuPWXPbNJpO8QSyvIF5T5n2IZiqX3A=
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E=
github.com/dsoprea/go-png-image-structure v0.0.0-20200807080309-a98d4e94ac82/go.mod h1:aDYQkL/5gfRNZkoxiLTSWU4Y8/gV/4MVsy/MU9uwTak= github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d h1:2zNIgrJTspLxUKoJGl0Ln24+hufPKSjP3cu4++5MeSE=
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d h1:8+qI8ant/vZkNSsbwSjIR6XJfWcDVTg/qx/3pRUUZNA= github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d/go.mod h1:scnx0wQSM7UiCMK66dSdiPZvL2hl6iF5DvpZ7uT59MY=
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d/go.mod h1:yTR3tKgyk20phAFg6IE9ulMA5NjEDD2wyx+okRFLVtw= github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf h1:/w4QxepU4AHh3AuO6/g8y/YIIHH5+aKP3Bj8sg5cqhU=
github.com/dsoprea/go-utility v0.0.0-20200512094054-1abbbc781176/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
github.com/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e h1:ojqYA1mU6LuRm8XzrVOvyfb000y59cbUcu6Wt8sFSAs= github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e h1:IxIbA7VbCNrwumIYjDoMOdf4KOSkMC6NJE4s8oRbE7E=
github.com/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo=
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-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@ -367,7 +352,6 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
@ -666,8 +650,8 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/superseriousbusiness/activity v1.0.1-0.20211113133524-56560b73ace8 h1:8Bwy6CSsT33/sF5FhjND4vr7jiJCaq4elNTAW4rUzVc= github.com/superseriousbusiness/activity v1.0.1-0.20211113133524-56560b73ace8 h1:8Bwy6CSsT33/sF5FhjND4vr7jiJCaq4elNTAW4rUzVc=
github.com/superseriousbusiness/activity v1.0.1-0.20211113133524-56560b73ace8/go.mod h1:ZY9xwFDucvp6zTvM6FQZGl8PSOofPBFIAy6gSc85XkY= github.com/superseriousbusiness/activity v1.0.1-0.20211113133524-56560b73ace8/go.mod h1:ZY9xwFDucvp6zTvM6FQZGl8PSOofPBFIAy6gSc85XkY=
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 h1:1SWXcTphBQjYGWRRxLFIAR1LVtQEj4eR7xPtyeOVM/c= github.com/superseriousbusiness/exif-terminator v0.1.0 h1:ePzfV0vcw+tm/haSOGzKbBTKkHAvyQLbCzfsdVkb3hM=
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203/go.mod h1:0Xw5cYMOYpgaWs+OOSx41ugycl2qvKTi9tlMMcZhFyY= github.com/superseriousbusiness/exif-terminator v0.1.0/go.mod h1:pmlOKzkFZWmqaucLAtrRbZG0R5F3dbrcLWOcd7gAOLI=
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB h1:PtW2w6budTvRV2J5QAoSvThTHBuvh8t/+BXIZFAaBSc= github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB h1:PtW2w6budTvRV2J5QAoSvThTHBuvh8t/+BXIZFAaBSc=
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB/go.mod h1:uYC/W92oVRJ49Vh1GcvTqpeFqHi+Ovrl2sMllQWRAEo= github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB/go.mod h1:uYC/W92oVRJ49Vh1GcvTqpeFqHi+Ovrl2sMllQWRAEo=
github.com/tdewolff/minify/v2 v2.9.22 h1:PlmaAakaJHdMMdTTwjjsuSwIxKqWPTlvjTj6a/g/ILU= github.com/tdewolff/minify/v2 v2.9.22 h1:PlmaAakaJHdMMdTTwjjsuSwIxKqWPTlvjTj6a/g/ILU=
@ -740,9 +724,11 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.1 h1:O+N0Y8Re2XAYjp0adlZDA2juyRguhMfPCgh8YIf7vyE= github.com/zeebo/blake3 v0.2.1 h1:O+N0Y8Re2XAYjp0adlZDA2juyRguhMfPCgh8YIf7vyE=
github.com/zeebo/blake3 v0.2.1/go.mod h1:TSQ0KjMH+pht+bRyvVooJ1rBpvvngSGaPISafq9MxJk= github.com/zeebo/blake3 v0.2.1/go.mod h1:TSQ0KjMH+pht+bRyvVooJ1rBpvvngSGaPISafq9MxJk=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=

View file

@ -1,24 +0,0 @@
language: go
go:
- master
- stable
- "1.14"
- "1.13"
- "1.12"
env:
- GO111MODULE=on
install:
- go get -t ./...
script:
# v1
- go test -v .
- go test -v ./exif-read-tool
# v2
- cd v2
- go test -v ./...
- cd ..
# v3. Coverage reports comes from this.
- cd v3
- go test -v ./... -coverprofile=coverage.txt -covermode=atomic
after_success:
- curl -s https://codecov.io/bash | bash

View file

@ -1,206 +0,0 @@
[![Build Status](https://travis-ci.org/dsoprea/go-exif.svg?branch=master)](https://travis-ci.org/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/github.com/dsoprea/go-exif/v3)](https://goreportcard.com/report/github.com/dsoprea/go-exif/v3)
[![GoDoc](https://godoc.org/github.com/dsoprea/go-exif/v3?status.svg)](https://godoc.org/github.com/dsoprea/go-exif/v3)
# 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:
```
$ go get -t github.com/dsoprea/go-exif/v3
```
# 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 get github.com/dsoprea/go-exif/v3/command/exif-read-tool
$ exif-read-tool --filepath "<media file-path>"
```
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 "<media file-path>" -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 github.com/dsoprea/go-exif/v3/...
```
# 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 478K
images.
[search-and-extract-exif]: https://godoc.org/github.com/dsoprea/go-exif/v3#SearchAndExtractExif
[search-file-and-extract-exif]: https://godoc.org/github.com/dsoprea/go-exif/v3#SearchFileAndExtractExif
[jpeg-set-exif]: https://godoc.org/github.com/dsoprea/go-jpeg-image-structure#example-SegmentList-SetExif
[examples]: https://godoc.org/github.com/dsoprea/go-exif/v3#pkg-examples
[releases]: https://github.com/dsoprea/go-exif/releases

View file

@ -1,10 +0,0 @@
package exif
import (
"errors"
)
var (
ErrTagNotFound = errors.New("tag not found")
ErrTagNotStandard = errors.New("tag not a standard tag")
)

View file

@ -1,247 +0,0 @@
package exif
import (
"bytes"
"errors"
"fmt"
"os"
"encoding/binary"
"io/ioutil"
"github.com/dsoprea/go-logging"
)
const (
// ExifAddressableAreaStart is the absolute offset in the file that all
// offsets are relative to.
ExifAddressableAreaStart = uint32(0x0)
// ExifDefaultFirstIfdOffset is essentially the number of bytes in addition
// to `ExifAddressableAreaStart` that you have to move in order to escape
// the rest of the header and get to the earliest point where we can put
// stuff (which has to be the first IFD). This is the size of the header
// sequence containing the two-character byte-order, two-character fixed-
// bytes, and the four bytes describing the first-IFD offset.
ExifDefaultFirstIfdOffset = uint32(2 + 2 + 4)
)
var (
exifLogger = log.NewLogger("exif.exif")
// EncodeDefaultByteOrder is the default byte-order for encoding operations.
EncodeDefaultByteOrder = binary.BigEndian
// Default byte order for tests.
TestDefaultByteOrder = binary.BigEndian
BigEndianBoBytes = [2]byte{'M', 'M'}
LittleEndianBoBytes = [2]byte{'I', 'I'}
ByteOrderLookup = map[[2]byte]binary.ByteOrder{
BigEndianBoBytes: binary.BigEndian,
LittleEndianBoBytes: binary.LittleEndian,
}
ByteOrderLookupR = map[binary.ByteOrder][2]byte{
binary.BigEndian: BigEndianBoBytes,
binary.LittleEndian: LittleEndianBoBytes,
}
ExifFixedBytesLookup = map[binary.ByteOrder][2]byte{
binary.LittleEndian: {0x2a, 0x00},
binary.BigEndian: {0x00, 0x2a},
}
)
var (
ErrNoExif = errors.New("no exif data")
ErrExifHeaderError = errors.New("exif header error")
)
// SearchAndExtractExif returns a slice from the beginning of the EXIF data to
// end of the file (it's not practical to try and calculate where the data
// actually ends; it needs to be formally parsed).
func SearchAndExtractExif(data []byte) (rawExif []byte, err error) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
// Search for the beginning of the EXIF information. The EXIF is near the
// beginning of our/most JPEGs, so this has a very low cost.
foundAt := -1
for i := 0; i < len(data); i++ {
if _, err := ParseExifHeader(data[i:]); err == nil {
foundAt = i
break
} else if log.Is(err, ErrNoExif) == false {
return nil, err
}
}
if foundAt == -1 {
return nil, ErrNoExif
}
return data[foundAt:], nil
}
// SearchFileAndExtractExif returns a slice from the beginning of the EXIF data
// to the end of the file (it's not practical to try and calculate where the
// data actually ends).
func SearchFileAndExtractExif(filepath string) (rawExif []byte, err error) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
// Open the file.
f, err := os.Open(filepath)
log.PanicIf(err)
defer f.Close()
data, err := ioutil.ReadAll(f)
log.PanicIf(err)
rawExif, err = SearchAndExtractExif(data)
log.PanicIf(err)
return rawExif, nil
}
type ExifHeader struct {
ByteOrder binary.ByteOrder
FirstIfdOffset uint32
}
func (eh ExifHeader) String() string {
return fmt.Sprintf("ExifHeader<BYTE-ORDER=[%v] FIRST-IFD-OFFSET=(0x%02x)>", eh.ByteOrder, eh.FirstIfdOffset)
}
// ParseExifHeader parses the bytes at the very top of the header.
//
// This will panic with ErrNoExif on any data errors so that we can double as
// an EXIF-detection routine.
func ParseExifHeader(data []byte) (eh ExifHeader, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// Good reference:
//
// CIPA DC-008-2016; JEITA CP-3451D
// -> http://www.cipa.jp/std/documents/e/DC-008-Translation-2016-E.pdf
if len(data) < 2 {
exifLogger.Warningf(nil, "Not enough data for EXIF header (1): (%d)", len(data))
return eh, ErrNoExif
}
byteOrderBytes := [2]byte{data[0], data[1]}
byteOrder, found := ByteOrderLookup[byteOrderBytes]
if found == false {
// exifLogger.Warningf(nil, "EXIF byte-order not recognized: [%v]", byteOrderBytes)
return eh, ErrNoExif
}
if len(data) < 4 {
exifLogger.Warningf(nil, "Not enough data for EXIF header (2): (%d)", len(data))
return eh, ErrNoExif
}
fixedBytes := [2]byte{data[2], data[3]}
expectedFixedBytes := ExifFixedBytesLookup[byteOrder]
if fixedBytes != expectedFixedBytes {
// exifLogger.Warningf(nil, "EXIF header fixed-bytes should be [%v] but are: [%v]", expectedFixedBytes, fixedBytes)
return eh, ErrNoExif
}
if len(data) < 2 {
exifLogger.Warningf(nil, "Not enough data for EXIF header (3): (%d)", len(data))
return eh, ErrNoExif
}
firstIfdOffset := byteOrder.Uint32(data[4:8])
eh = ExifHeader{
ByteOrder: byteOrder,
FirstIfdOffset: firstIfdOffset,
}
return eh, nil
}
// Visit recursively invokes a callback for every tag.
func Visit(rootIfdName string, ifdMapping *IfdMapping, tagIndex *TagIndex, exifData []byte, visitor RawTagVisitor) (eh ExifHeader, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
eh, err = ParseExifHeader(exifData)
log.PanicIf(err)
ie := NewIfdEnumerate(ifdMapping, tagIndex, exifData, eh.ByteOrder)
err = ie.Scan(rootIfdName, eh.FirstIfdOffset, visitor, true)
log.PanicIf(err)
return eh, nil
}
// Collect recursively builds a static structure of all IFDs and tags.
func Collect(ifdMapping *IfdMapping, tagIndex *TagIndex, exifData []byte) (eh ExifHeader, index IfdIndex, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
eh, err = ParseExifHeader(exifData)
log.PanicIf(err)
ie := NewIfdEnumerate(ifdMapping, tagIndex, exifData, eh.ByteOrder)
index, err = ie.Collect(eh.FirstIfdOffset, true)
log.PanicIf(err)
return eh, index, nil
}
// BuildExifHeader constructs the bytes that go in the very beginning.
func BuildExifHeader(byteOrder binary.ByteOrder, firstIfdOffset uint32) (headerBytes []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
b := new(bytes.Buffer)
// This is the point in the data that all offsets are relative to.
boBytes := ByteOrderLookupR[byteOrder]
_, err = b.WriteString(string(boBytes[:]))
log.PanicIf(err)
fixedBytes := ExifFixedBytesLookup[byteOrder]
_, err = b.Write(fixedBytes[:])
log.PanicIf(err)
err = binary.Write(b, byteOrder, firstIfdOffset)
log.PanicIf(err)
return b.Bytes(), nil
}

View file

@ -1,56 +0,0 @@
package exif
import (
"errors"
"fmt"
"time"
"github.com/golang/geo/s2"
)
var (
ErrGpsCoordinatesNotValid = errors.New("GPS coordinates not valid")
)
type GpsDegrees struct {
Orientation byte
Degrees, Minutes, Seconds float64
}
func (d GpsDegrees) String() string {
return fmt.Sprintf("Degrees<O=[%s] D=(%g) M=(%g) S=(%g)>", string([]byte{d.Orientation}), d.Degrees, d.Minutes, d.Seconds)
}
func (d GpsDegrees) Decimal() float64 {
decimal := float64(d.Degrees) + float64(d.Minutes)/60.0 + float64(d.Seconds)/3600.0
if d.Orientation == 'S' || d.Orientation == 'W' {
return -decimal
} else {
return decimal
}
}
type GpsInfo struct {
Latitude, Longitude GpsDegrees
Altitude int
Timestamp time.Time
}
func (gi *GpsInfo) String() string {
return fmt.Sprintf("GpsInfo<LAT=(%.05f) LON=(%.05f) ALT=(%d) TIME=[%s]>", gi.Latitude.Decimal(), gi.Longitude.Decimal(), gi.Altitude, gi.Timestamp)
}
func (gi *GpsInfo) S2CellId() s2.CellID {
latitude := gi.Latitude.Decimal()
longitude := gi.Longitude.Decimal()
ll := s2.LatLngFromDegrees(latitude, longitude)
cellId := s2.CellIDFromLatLng(ll)
if cellId.IsValid() == false {
panic(ErrGpsCoordinatesNotValid)
}
return cellId
}

View file

@ -1,407 +0,0 @@
package exif
import (
"errors"
"fmt"
"strings"
"github.com/dsoprea/go-logging"
)
const (
// IFD names. The paths that we referred to the IFDs with are comprised of
// these.
IfdStandard = "IFD"
IfdExif = "Exif"
IfdGps = "GPSInfo"
IfdIop = "Iop"
// Tag IDs for child IFDs.
IfdExifId = 0x8769
IfdGpsId = 0x8825
IfdIopId = 0xA005
// Just a placeholder.
IfdRootId = 0x0000
// The paths of the standard IFDs expressed in the standard IFD-mappings
// and as the group-names in the tag data.
IfdPathStandard = "IFD"
IfdPathStandardExif = "IFD/Exif"
IfdPathStandardExifIop = "IFD/Exif/Iop"
IfdPathStandardGps = "IFD/GPSInfo"
)
var (
ifdLogger = log.NewLogger("exif.ifd")
)
var (
ErrChildIfdNotMapped = errors.New("no child-IFD for that tag-ID under parent")
)
// type IfdIdentity struct {
// ParentIfdName string
// IfdName string
// }
// func (ii IfdIdentity) String() string {
// return fmt.Sprintf("IfdIdentity<PARENT-NAME=[%s] NAME=[%s]>", ii.ParentIfdName, ii.IfdName)
// }
type MappedIfd struct {
ParentTagId uint16
Placement []uint16
Path []string
Name string
TagId uint16
Children map[uint16]*MappedIfd
}
func (mi *MappedIfd) String() string {
pathPhrase := mi.PathPhrase()
return fmt.Sprintf("MappedIfd<(0x%04X) [%s] PATH=[%s]>", mi.TagId, mi.Name, pathPhrase)
}
func (mi *MappedIfd) PathPhrase() string {
return strings.Join(mi.Path, "/")
}
// IfdMapping describes all of the IFDs that we currently recognize.
type IfdMapping struct {
rootNode *MappedIfd
}
func NewIfdMapping() (ifdMapping *IfdMapping) {
rootNode := &MappedIfd{
Path: make([]string, 0),
Children: make(map[uint16]*MappedIfd),
}
return &IfdMapping{
rootNode: rootNode,
}
}
func NewIfdMappingWithStandard() (ifdMapping *IfdMapping) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
return im
}
func (im *IfdMapping) Get(parentPlacement []uint16) (childIfd *MappedIfd, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ptr := im.rootNode
for _, tagId := range parentPlacement {
if descendantPtr, found := ptr.Children[tagId]; found == false {
log.Panicf("ifd child with tag-ID (%04x) not registered: [%s]", tagId, ptr.PathPhrase())
} else {
ptr = descendantPtr
}
}
return ptr, nil
}
func (im *IfdMapping) GetWithPath(pathPhrase string) (mi *MappedIfd, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if pathPhrase == "" {
log.Panicf("path-phrase is empty")
}
path := strings.Split(pathPhrase, "/")
ptr := im.rootNode
for _, name := range path {
var hit *MappedIfd
for _, mi := range ptr.Children {
if mi.Name == name {
hit = mi
break
}
}
if hit == nil {
log.Panicf("ifd child with name [%s] not registered: [%s]", name, ptr.PathPhrase())
}
ptr = hit
}
return ptr, nil
}
// GetChild is a convenience function to get the child path for a given parent
// placement and child tag-ID.
func (im *IfdMapping) GetChild(parentPathPhrase string, tagId uint16) (mi *MappedIfd, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
mi, err = im.GetWithPath(parentPathPhrase)
log.PanicIf(err)
for _, childMi := range mi.Children {
if childMi.TagId == tagId {
return childMi, nil
}
}
// Whether or not an IFD is defined in data, such an IFD is not registered
// and would be unknown.
log.Panic(ErrChildIfdNotMapped)
return nil, nil
}
type IfdTagIdAndIndex struct {
Name string
TagId uint16
Index int
}
func (itii IfdTagIdAndIndex) String() string {
return fmt.Sprintf("IfdTagIdAndIndex<NAME=[%s] ID=(%04x) INDEX=(%d)>", itii.Name, itii.TagId, itii.Index)
}
// ResolvePath takes a list of names, which can also be suffixed with indices
// (to identify the second, third, etc.. sibling IFD) and returns a list of
// tag-IDs and those indices.
//
// Example:
//
// - IFD/Exif/Iop
// - IFD0/Exif/Iop
//
// This is the only call that supports adding the numeric indices.
func (im *IfdMapping) ResolvePath(pathPhrase string) (lineage []IfdTagIdAndIndex, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
pathPhrase = strings.TrimSpace(pathPhrase)
if pathPhrase == "" {
log.Panicf("can not resolve empty path-phrase")
}
path := strings.Split(pathPhrase, "/")
lineage = make([]IfdTagIdAndIndex, len(path))
ptr := im.rootNode
empty := IfdTagIdAndIndex{}
for i, name := range path {
indexByte := name[len(name)-1]
index := 0
if indexByte >= '0' && indexByte <= '9' {
index = int(indexByte - '0')
name = name[:len(name)-1]
}
itii := IfdTagIdAndIndex{}
for _, mi := range ptr.Children {
if mi.Name != name {
continue
}
itii.Name = name
itii.TagId = mi.TagId
itii.Index = index
ptr = mi
break
}
if itii == empty {
log.Panicf("ifd child with name [%s] not registered: [%s]", name, pathPhrase)
}
lineage[i] = itii
}
return lineage, nil
}
func (im *IfdMapping) FqPathPhraseFromLineage(lineage []IfdTagIdAndIndex) (fqPathPhrase string) {
fqPathParts := make([]string, len(lineage))
for i, itii := range lineage {
if itii.Index > 0 {
fqPathParts[i] = fmt.Sprintf("%s%d", itii.Name, itii.Index)
} else {
fqPathParts[i] = itii.Name
}
}
return strings.Join(fqPathParts, "/")
}
func (im *IfdMapping) PathPhraseFromLineage(lineage []IfdTagIdAndIndex) (pathPhrase string) {
pathParts := make([]string, len(lineage))
for i, itii := range lineage {
pathParts[i] = itii.Name
}
return strings.Join(pathParts, "/")
}
// StripPathPhraseIndices returns a non-fully-qualified path-phrase (no
// indices).
func (im *IfdMapping) StripPathPhraseIndices(pathPhrase string) (strippedPathPhrase string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
lineage, err := im.ResolvePath(pathPhrase)
log.PanicIf(err)
strippedPathPhrase = im.PathPhraseFromLineage(lineage)
return strippedPathPhrase, nil
}
// Add puts the given IFD at the given position of the tree. The position of the
// tree is referred to as the placement and is represented by a set of tag-IDs,
// where the leftmost is the root tag and the tags going to the right are
// progressive descendants.
func (im *IfdMapping) Add(parentPlacement []uint16, tagId uint16, name string) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): !! It would be nicer to provide a list of names in the placement rather than tag-IDs.
ptr, err := im.Get(parentPlacement)
log.PanicIf(err)
path := make([]string, len(parentPlacement)+1)
if len(parentPlacement) > 0 {
copy(path, ptr.Path)
}
path[len(path)-1] = name
placement := make([]uint16, len(parentPlacement)+1)
if len(placement) > 0 {
copy(placement, ptr.Placement)
}
placement[len(placement)-1] = tagId
childIfd := &MappedIfd{
ParentTagId: ptr.TagId,
Path: path,
Placement: placement,
Name: name,
TagId: tagId,
Children: make(map[uint16]*MappedIfd),
}
if _, found := ptr.Children[tagId]; found == true {
log.Panicf("child IFD with tag-ID (%04x) already registered under IFD [%s] with tag-ID (%04x)", tagId, ptr.Name, ptr.TagId)
}
ptr.Children[tagId] = childIfd
return nil
}
func (im *IfdMapping) dumpLineages(stack []*MappedIfd, input []string) (output []string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
currentIfd := stack[len(stack)-1]
output = input
for _, childIfd := range currentIfd.Children {
stackCopy := make([]*MappedIfd, len(stack)+1)
copy(stackCopy, stack)
stackCopy[len(stack)] = childIfd
// Add to output, but don't include the obligatory root node.
parts := make([]string, len(stackCopy)-1)
for i, mi := range stackCopy[1:] {
parts[i] = mi.Name
}
output = append(output, strings.Join(parts, "/"))
output, err = im.dumpLineages(stackCopy, output)
log.PanicIf(err)
}
return output, nil
}
func (im *IfdMapping) DumpLineages() (output []string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
stack := []*MappedIfd{im.rootNode}
output = make([]string, 0)
output, err = im.dumpLineages(stack, output)
log.PanicIf(err)
return output, nil
}
func LoadStandardIfds(im *IfdMapping) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
err = im.Add([]uint16{}, IfdRootId, IfdStandard)
log.PanicIf(err)
err = im.Add([]uint16{IfdRootId}, IfdExifId, IfdExif)
log.PanicIf(err)
err = im.Add([]uint16{IfdRootId, IfdExifId}, IfdIopId, IfdIop)
log.PanicIf(err)
err = im.Add([]uint16{IfdRootId}, IfdGpsId, IfdGps)
log.PanicIf(err)
return nil
}

File diff suppressed because it is too large Load diff

View file

@ -1,530 +0,0 @@
package exif
import (
"bytes"
"fmt"
"strings"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
const (
// Tag-ID + Tag-Type + Unit-Count + Value/Offset.
IfdTagEntrySize = uint32(2 + 2 + 4 + 4)
)
type ByteWriter struct {
b *bytes.Buffer
byteOrder binary.ByteOrder
}
func NewByteWriter(b *bytes.Buffer, byteOrder binary.ByteOrder) (bw *ByteWriter) {
return &ByteWriter{
b: b,
byteOrder: byteOrder,
}
}
func (bw ByteWriter) writeAsBytes(value interface{}) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
err = binary.Write(bw.b, bw.byteOrder, value)
log.PanicIf(err)
return nil
}
func (bw ByteWriter) WriteUint32(value uint32) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
err = bw.writeAsBytes(value)
log.PanicIf(err)
return nil
}
func (bw ByteWriter) WriteUint16(value uint16) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
err = bw.writeAsBytes(value)
log.PanicIf(err)
return nil
}
func (bw ByteWriter) WriteFourBytes(value []byte) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
len_ := len(value)
if len_ != 4 {
log.Panicf("value is not four-bytes: (%d)", len_)
}
_, err = bw.b.Write(value)
log.PanicIf(err)
return nil
}
// ifdOffsetIterator keeps track of where the next IFD should be written by
// keeping track of where the offsets start, the data that has been added, and
// bumping the offset *when* the data is added.
type ifdDataAllocator struct {
offset uint32
b bytes.Buffer
}
func newIfdDataAllocator(ifdDataAddressableOffset uint32) *ifdDataAllocator {
return &ifdDataAllocator{
offset: ifdDataAddressableOffset,
}
}
func (ida *ifdDataAllocator) Allocate(value []byte) (offset uint32, err error) {
_, err = ida.b.Write(value)
log.PanicIf(err)
offset = ida.offset
ida.offset += uint32(len(value))
return offset, nil
}
func (ida *ifdDataAllocator) NextOffset() uint32 {
return ida.offset
}
func (ida *ifdDataAllocator) Bytes() []byte {
return ida.b.Bytes()
}
// IfdByteEncoder converts an IB to raw bytes (for writing) while also figuring
// out all of the allocations and indirection that is required for extended
// data.
type IfdByteEncoder struct {
// journal holds a list of actions taken while encoding.
journal [][3]string
}
func NewIfdByteEncoder() (ibe *IfdByteEncoder) {
return &IfdByteEncoder{
journal: make([][3]string, 0),
}
}
func (ibe *IfdByteEncoder) Journal() [][3]string {
return ibe.journal
}
func (ibe *IfdByteEncoder) TableSize(entryCount int) uint32 {
// Tag-Count + (Entry-Size * Entry-Count) + Next-IFD-Offset.
return uint32(2) + (IfdTagEntrySize * uint32(entryCount)) + uint32(4)
}
func (ibe *IfdByteEncoder) pushToJournal(where, direction, format string, args ...interface{}) {
event := [3]string{
direction,
where,
fmt.Sprintf(format, args...),
}
ibe.journal = append(ibe.journal, event)
}
// PrintJournal prints a hierarchical representation of the steps taken during
// encoding.
func (ibe *IfdByteEncoder) PrintJournal() {
maxWhereLength := 0
for _, event := range ibe.journal {
where := event[1]
len_ := len(where)
if len_ > maxWhereLength {
maxWhereLength = len_
}
}
level := 0
for i, event := range ibe.journal {
direction := event[0]
where := event[1]
message := event[2]
if direction != ">" && direction != "<" && direction != "-" {
log.Panicf("journal operation not valid: [%s]", direction)
}
if direction == "<" {
if level <= 0 {
log.Panicf("journal operations unbalanced (too many closes)")
}
level--
}
indent := strings.Repeat(" ", level)
fmt.Printf("%3d %s%s %s: %s\n", i, indent, direction, where, message)
if direction == ">" {
level++
}
}
if level != 0 {
log.Panicf("journal operations unbalanced (too many opens)")
}
}
// encodeTagToBytes encodes the given tag to a byte stream. If
// `nextIfdOffsetToWrite` is more than (0), recurse into child IFDs
// (`nextIfdOffsetToWrite` is required in order for them to know where the its
// IFD data will be written, in order for them to know the offset of where
// their allocated-data block will start, which follows right behind).
func (ibe *IfdByteEncoder) encodeTagToBytes(ib *IfdBuilder, bt *BuilderTag, bw *ByteWriter, ida *ifdDataAllocator, nextIfdOffsetToWrite uint32) (childIfdBlock []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// Write tag-ID.
err = bw.WriteUint16(bt.tagId)
log.PanicIf(err)
// Works for both values and child IFDs (which have an official size of
// LONG).
err = bw.WriteUint16(uint16(bt.typeId))
log.PanicIf(err)
// Write unit-count.
if bt.value.IsBytes() == true {
effectiveType := bt.typeId
if bt.typeId == TypeUndefined {
effectiveType = TypeByte
}
// It's a non-unknown value.Calculate the count of values of
// the type that we're writing and the raw bytes for the whole list.
typeSize := uint32(effectiveType.Size())
valueBytes := bt.value.Bytes()
len_ := len(valueBytes)
unitCount := uint32(len_) / typeSize
if _, found := tagsWithoutAlignment[bt.tagId]; found == false {
remainder := uint32(len_) % typeSize
if remainder > 0 {
log.Panicf("tag (0x%04x) value of (%d) bytes not evenly divisible by type-size (%d)", bt.tagId, len_, typeSize)
}
}
err = bw.WriteUint32(unitCount)
log.PanicIf(err)
// Write four-byte value/offset.
if len_ > 4 {
offset, err := ida.Allocate(valueBytes)
log.PanicIf(err)
err = bw.WriteUint32(offset)
log.PanicIf(err)
} else {
fourBytes := make([]byte, 4)
copy(fourBytes, valueBytes)
err = bw.WriteFourBytes(fourBytes)
log.PanicIf(err)
}
} else {
if bt.value.IsIb() == false {
log.Panicf("tag value is not a byte-slice but also not a child IB: %v", bt)
}
// Write unit-count (one LONG representing one offset).
err = bw.WriteUint32(1)
log.PanicIf(err)
if nextIfdOffsetToWrite > 0 {
var err error
ibe.pushToJournal("encodeTagToBytes", ">", "[%s]->[%s]", ib.ifdPath, bt.value.Ib().ifdPath)
// Create the block of IFD data and everything it requires.
childIfdBlock, err = ibe.encodeAndAttachIfd(bt.value.Ib(), nextIfdOffsetToWrite)
log.PanicIf(err)
ibe.pushToJournal("encodeTagToBytes", "<", "[%s]->[%s]", bt.value.Ib().ifdPath, ib.ifdPath)
// Use the next-IFD offset for it. The IFD will actually get
// attached after we return.
err = bw.WriteUint32(nextIfdOffsetToWrite)
log.PanicIf(err)
} else {
// No child-IFDs are to be allocated. Finish the entry with a NULL
// pointer.
ibe.pushToJournal("encodeTagToBytes", "-", "*Not* descending to child: [%s]", bt.value.Ib().ifdPath)
err = bw.WriteUint32(0)
log.PanicIf(err)
}
}
return childIfdBlock, nil
}
// encodeIfdToBytes encodes the given IB to a byte-slice. We are given the
// offset at which this IFD will be written. This method is used called both to
// pre-determine how big the table is going to be (so that we can calculate the
// address to allocate data at) as well as to write the final table.
//
// It is necessary to fully realize the table in order to predetermine its size
// because it is not enough to know the size of the table: If there are child
// IFDs, we will not be able to allocate them without first knowing how much
// data we need to allocate for the current IFD.
func (ibe *IfdByteEncoder) encodeIfdToBytes(ib *IfdBuilder, ifdAddressableOffset uint32, nextIfdOffsetToWrite uint32, setNextIb bool) (data []byte, tableSize uint32, dataSize uint32, childIfdSizes []uint32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ibe.pushToJournal("encodeIfdToBytes", ">", "%s", ib)
tableSize = ibe.TableSize(len(ib.tags))
b := new(bytes.Buffer)
bw := NewByteWriter(b, ib.byteOrder)
// Write tag count.
err = bw.WriteUint16(uint16(len(ib.tags)))
log.PanicIf(err)
ida := newIfdDataAllocator(ifdAddressableOffset)
childIfdBlocks := make([][]byte, 0)
// Write raw bytes for each tag entry. Allocate larger data to be referred
// to in the follow-up data-block as required. Any "unknown"-byte tags that
// we can't parse will not be present here (using AddTagsFromExisting(), at
// least).
for _, bt := range ib.tags {
childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, nextIfdOffsetToWrite)
log.PanicIf(err)
if childIfdBlock != nil {
// We aren't allowed to have non-nil child IFDs if we're just
// sizing things up.
if nextIfdOffsetToWrite == 0 {
log.Panicf("no IFD offset provided for child-IFDs; no new child-IFDs permitted")
}
nextIfdOffsetToWrite += uint32(len(childIfdBlock))
childIfdBlocks = append(childIfdBlocks, childIfdBlock)
}
}
dataBytes := ida.Bytes()
dataSize = uint32(len(dataBytes))
childIfdSizes = make([]uint32, len(childIfdBlocks))
childIfdsTotalSize := uint32(0)
for i, childIfdBlock := range childIfdBlocks {
len_ := uint32(len(childIfdBlock))
childIfdSizes[i] = len_
childIfdsTotalSize += len_
}
// N the link from this IFD to the next IFD that will be written in the
// next cycle.
if setNextIb == true {
// Write address of next IFD in chain. This will be the original
// allocation offset plus the size of everything we have allocated for
// this IFD and its child-IFDs.
//
// It is critical that this number is stepped properly. We experienced
// an issue whereby it first looked like we were duplicating the IFD and
// then that we were duplicating the tags in the wrong IFD, and then
// finally we determined that the next-IFD offset for the first IFD was
// accidentally pointing back to the EXIF IFD, so we were visiting it
// twice when visiting through the tags after decoding. It was an
// expensive bug to find.
ibe.pushToJournal("encodeIfdToBytes", "-", "Setting 'next' IFD to (0x%08x).", nextIfdOffsetToWrite)
err := bw.WriteUint32(nextIfdOffsetToWrite)
log.PanicIf(err)
} else {
err := bw.WriteUint32(0)
log.PanicIf(err)
}
_, err = b.Write(dataBytes)
log.PanicIf(err)
// Append any child IFD blocks after our table and data blocks. These IFDs
// were equipped with the appropriate offset information so it's expected
// that all offsets referred to by these will be correct.
//
// Note that child-IFDs are append after the current IFD and before the
// next IFD, as opposed to the root IFDs, which are chained together but
// will be interrupted by these child-IFDs (which is expected, per the
// standard).
for _, childIfdBlock := range childIfdBlocks {
_, err = b.Write(childIfdBlock)
log.PanicIf(err)
}
ibe.pushToJournal("encodeIfdToBytes", "<", "%s", ib)
return b.Bytes(), tableSize, dataSize, childIfdSizes, nil
}
// encodeAndAttachIfd is a reentrant function that processes the IFD chain.
func (ibe *IfdByteEncoder) encodeAndAttachIfd(ib *IfdBuilder, ifdAddressableOffset uint32) (data []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ibe.pushToJournal("encodeAndAttachIfd", ">", "%s", ib)
b := new(bytes.Buffer)
i := 0
for thisIb := ib; thisIb != nil; thisIb = thisIb.nextIb {
// Do a dry-run in order to pre-determine its size requirement.
ibe.pushToJournal("encodeAndAttachIfd", ">", "Beginning encoding process: (%d) [%s]", i, thisIb.ifdPath)
ibe.pushToJournal("encodeAndAttachIfd", ">", "Calculating size: (%d) [%s]", i, thisIb.ifdPath)
_, tableSize, allocatedDataSize, _, err := ibe.encodeIfdToBytes(thisIb, ifdAddressableOffset, 0, false)
log.PanicIf(err)
ibe.pushToJournal("encodeAndAttachIfd", "<", "Finished calculating size: (%d) [%s]", i, thisIb.ifdPath)
ifdAddressableOffset += tableSize
nextIfdOffsetToWrite := ifdAddressableOffset + allocatedDataSize
ibe.pushToJournal("encodeAndAttachIfd", ">", "Next IFD will be written at offset (0x%08x)", nextIfdOffsetToWrite)
// Write our IFD as well as any child-IFDs (now that we know the offset
// where new IFDs and their data will be allocated).
setNextIb := thisIb.nextIb != nil
ibe.pushToJournal("encodeAndAttachIfd", ">", "Encoding starting: (%d) [%s] NEXT-IFD-OFFSET-TO-WRITE=(0x%08x)", i, thisIb.ifdPath, nextIfdOffsetToWrite)
tableAndAllocated, effectiveTableSize, effectiveAllocatedDataSize, childIfdSizes, err :=
ibe.encodeIfdToBytes(thisIb, ifdAddressableOffset, nextIfdOffsetToWrite, setNextIb)
log.PanicIf(err)
if effectiveTableSize != tableSize {
log.Panicf("written table size does not match the pre-calculated table size: (%d) != (%d) %s", effectiveTableSize, tableSize, ib)
} else if effectiveAllocatedDataSize != allocatedDataSize {
log.Panicf("written allocated-data size does not match the pre-calculated allocated-data size: (%d) != (%d) %s", effectiveAllocatedDataSize, allocatedDataSize, ib)
}
ibe.pushToJournal("encodeAndAttachIfd", "<", "Encoding done: (%d) [%s]", i, thisIb.ifdPath)
totalChildIfdSize := uint32(0)
for _, childIfdSize := range childIfdSizes {
totalChildIfdSize += childIfdSize
}
if len(tableAndAllocated) != int(tableSize+allocatedDataSize+totalChildIfdSize) {
log.Panicf("IFD table and data is not a consistent size: (%d) != (%d)", len(tableAndAllocated), tableSize+allocatedDataSize+totalChildIfdSize)
}
// TODO(dustin): We might want to verify the original tableAndAllocated length, too.
_, err = b.Write(tableAndAllocated)
log.PanicIf(err)
// Advance past what we've allocated, thus far.
ifdAddressableOffset += allocatedDataSize + totalChildIfdSize
ibe.pushToJournal("encodeAndAttachIfd", "<", "Finishing encoding process: (%d) [%s] [FINAL:] NEXT-IFD-OFFSET-TO-WRITE=(0x%08x)", i, ib.ifdPath, nextIfdOffsetToWrite)
i++
}
ibe.pushToJournal("encodeAndAttachIfd", "<", "%s", ib)
return b.Bytes(), nil
}
// EncodeToExifPayload is the base encoding step that transcribes the entire IB
// structure to its on-disk layout.
func (ibe *IfdByteEncoder) EncodeToExifPayload(ib *IfdBuilder) (data []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
data, err = ibe.encodeAndAttachIfd(ib, ExifDefaultFirstIfdOffset)
log.PanicIf(err)
return data, nil
}
// EncodeToExif calls EncodeToExifPayload and then packages the result into a
// complete EXIF block.
func (ibe *IfdByteEncoder) EncodeToExif(ib *IfdBuilder) (data []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
encodedIfds, err := ibe.EncodeToExifPayload(ib)
log.PanicIf(err)
// Wrap the IFD in a formal EXIF block.
b := new(bytes.Buffer)
headerBytes, err := BuildExifHeader(ib.byteOrder, ExifDefaultFirstIfdOffset)
log.PanicIf(err)
_, err = b.Write(headerBytes)
log.PanicIf(err)
_, err = b.Write(encodedIfds)
log.PanicIf(err)
return b.Bytes(), nil
}

File diff suppressed because it is too large Load diff

View file

@ -1,233 +0,0 @@
package exif
import (
"fmt"
"reflect"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
var (
iteLogger = log.NewLogger("exif.ifd_tag_entry")
)
type IfdTagEntry struct {
TagId uint16
TagIndex int
TagType TagTypePrimitive
UnitCount uint32
ValueOffset uint32
RawValueOffset []byte
// ChildIfdName is the right most atom in the IFD-path. We need this to
// construct the fully-qualified IFD-path.
ChildIfdName string
// ChildIfdPath is the IFD-path of the child if this tag represents a child
// IFD.
ChildIfdPath string
// ChildFqIfdPath is the IFD-path of the child if this tag represents a
// child IFD. Includes indices.
ChildFqIfdPath string
// TODO(dustin): !! IB's host the child-IBs directly in the tag, but that's not the case here. Refactor to accomodate it for a consistent experience.
// IfdPath is the IFD that this tag belongs to.
IfdPath string
// TODO(dustin): !! We now parse and read the value immediately. Update the rest of the logic to use this and get rid of all of the staggered and different resolution mechanisms.
value []byte
isUnhandledUnknown bool
}
func (ite *IfdTagEntry) String() string {
return fmt.Sprintf("IfdTagEntry<TAG-IFD-PATH=[%s] TAG-ID=(0x%04x) TAG-TYPE=[%s] UNIT-COUNT=(%d)>", ite.IfdPath, ite.TagId, TypeNames[ite.TagType], ite.UnitCount)
}
// TODO(dustin): TODO(dustin): Stop exporting IfdPath and TagId.
//
// func (ite *IfdTagEntry) IfdPath() string {
// return ite.IfdPath
// }
// TODO(dustin): TODO(dustin): Stop exporting IfdPath and TagId.
//
// func (ite *IfdTagEntry) TagId() uint16 {
// return ite.TagId
// }
// ValueString renders a string from whatever the value in this tag is.
func (ite *IfdTagEntry) ValueString(addressableData []byte, byteOrder binary.ByteOrder) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
valueContext :=
newValueContextFromTag(
ite,
addressableData,
byteOrder)
if ite.TagType == TypeUndefined {
valueRaw, err := valueContext.Undefined()
log.PanicIf(err)
value = fmt.Sprintf("%v", valueRaw)
} else {
value, err = valueContext.Format()
log.PanicIf(err)
}
return value, nil
}
// ValueBytes renders a specific list of bytes from the value in this tag.
func (ite *IfdTagEntry) ValueBytes(addressableData []byte, byteOrder binary.ByteOrder) (value []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// Return the exact bytes of the unknown-type value. Returning a string
// (`ValueString`) is easy because we can just pass everything to
// `Sprintf()`. Returning the raw, typed value (`Value`) is easy
// (obviously). However, here, in order to produce the list of bytes, we
// need to coerce whatever `Undefined()` returns.
if ite.TagType == TypeUndefined {
valueContext :=
newValueContextFromTag(
ite,
addressableData,
byteOrder)
value, err := valueContext.Undefined()
log.PanicIf(err)
switch value.(type) {
case []byte:
return value.([]byte), nil
case TagUnknownType_UnknownValue:
b := []byte(value.(TagUnknownType_UnknownValue))
return b, nil
case string:
return []byte(value.(string)), nil
case UnknownTagValue:
valueBytes, err := value.(UnknownTagValue).ValueBytes()
log.PanicIf(err)
return valueBytes, nil
default:
// TODO(dustin): !! Finish translating the rest of the types (make reusable and replace into other similar implementations?)
log.Panicf("can not produce bytes for unknown-type tag (0x%04x) (2): [%s]", ite.TagId, reflect.TypeOf(value))
}
}
originalType := NewTagType(ite.TagType, byteOrder)
byteCount := uint32(originalType.Type().Size()) * ite.UnitCount
tt := NewTagType(TypeByte, byteOrder)
if tt.valueIsEmbedded(byteCount) == true {
iteLogger.Debugf(nil, "Reading BYTE value (ITE; embedded).")
// In this case, the bytes normally used for the offset are actually
// data.
value, err = tt.ParseBytes(ite.RawValueOffset, byteCount)
log.PanicIf(err)
} else {
iteLogger.Debugf(nil, "Reading BYTE value (ITE; at offset).")
value, err = tt.ParseBytes(addressableData[ite.ValueOffset:], byteCount)
log.PanicIf(err)
}
return value, nil
}
// Value returns the specific, parsed, typed value from the tag.
func (ite *IfdTagEntry) Value(addressableData []byte, byteOrder binary.ByteOrder) (value interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
valueContext :=
newValueContextFromTag(
ite,
addressableData,
byteOrder)
if ite.TagType == TypeUndefined {
value, err = valueContext.Undefined()
log.PanicIf(err)
} else {
tt := NewTagType(ite.TagType, byteOrder)
value, err = tt.Resolve(valueContext)
log.PanicIf(err)
}
return value, nil
}
// IfdTagEntryValueResolver instances know how to resolve the values for any
// tag for a particular EXIF block.
type IfdTagEntryValueResolver struct {
addressableData []byte
byteOrder binary.ByteOrder
}
func NewIfdTagEntryValueResolver(exifData []byte, byteOrder binary.ByteOrder) (itevr *IfdTagEntryValueResolver) {
return &IfdTagEntryValueResolver{
addressableData: exifData[ExifAddressableAreaStart:],
byteOrder: byteOrder,
}
}
// ValueBytes will resolve embedded or allocated data from the tag and return the raw bytes.
func (itevr *IfdTagEntryValueResolver) ValueBytes(ite *IfdTagEntry) (value []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// OBSOLETE(dustin): This is now redundant. Use `(*ValueContext).readRawEncoded()` instead of this method.
valueContext := newValueContextFromTag(
ite,
itevr.addressableData,
itevr.byteOrder)
rawBytes, err := valueContext.readRawEncoded()
log.PanicIf(err)
return rawBytes, nil
}
func (itevr *IfdTagEntryValueResolver) Value(ite *IfdTagEntry) (value interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// OBSOLETE(dustin): This is now redundant. Use `(*ValueContext).Values()` instead of this method.
valueContext := newValueContextFromTag(
ite,
itevr.addressableData,
itevr.byteOrder)
values, err := valueContext.Values()
log.PanicIf(err)
return values, nil
}

View file

@ -1,4 +0,0 @@
// exif parses raw EXIF information given a block of raw EXIF data.
//
// v1 of go-exif is now deprecated. Please use v2.
package exif

View file

@ -1,190 +0,0 @@
package exif
import (
"bytes"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
type Parser struct {
}
func (p *Parser) ParseBytes(data []byte, unitCount uint32) (value []uint8, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeByte.Size() * count) {
log.Panic(ErrNotEnoughData)
}
value = []uint8(data[:count])
return value, nil
}
// ParseAscii returns a string and auto-strips the trailing NUL character.
func (p *Parser) ParseAscii(data []byte, unitCount uint32) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeAscii.Size() * count) {
log.Panic(ErrNotEnoughData)
}
if len(data) == 0 || data[count-1] != 0 {
s := string(data[:count])
typeLogger.Warningf(nil, "ascii not terminated with nul as expected: [%v]", s)
return s, nil
} else {
// Auto-strip the NUL from the end. It serves no purpose outside of
// encoding semantics.
return string(data[:count-1]), nil
}
}
// ParseAsciiNoNul returns a string without any consideration for a trailing NUL
// character.
func (p *Parser) ParseAsciiNoNul(data []byte, unitCount uint32) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeAscii.Size() * count) {
log.Panic(ErrNotEnoughData)
}
return string(data[:count]), nil
}
func (p *Parser) ParseShorts(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []uint16, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeShort.Size() * count) {
log.Panic(ErrNotEnoughData)
}
value = make([]uint16, count)
for i := 0; i < count; i++ {
value[i] = byteOrder.Uint16(data[i*2:])
}
return value, nil
}
func (p *Parser) ParseLongs(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []uint32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeLong.Size() * count) {
log.Panic(ErrNotEnoughData)
}
value = make([]uint32, count)
for i := 0; i < count; i++ {
value[i] = byteOrder.Uint32(data[i*4:])
}
return value, nil
}
func (p *Parser) ParseRationals(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []Rational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeRational.Size() * count) {
log.Panic(ErrNotEnoughData)
}
value = make([]Rational, count)
for i := 0; i < count; i++ {
value[i].Numerator = byteOrder.Uint32(data[i*8:])
value[i].Denominator = byteOrder.Uint32(data[i*8+4:])
}
return value, nil
}
func (p *Parser) ParseSignedLongs(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []int32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeSignedLong.Size() * count) {
log.Panic(ErrNotEnoughData)
}
b := bytes.NewBuffer(data)
value = make([]int32, count)
for i := 0; i < count; i++ {
err := binary.Read(b, byteOrder, &value[i])
log.PanicIf(err)
}
return value, nil
}
func (p *Parser) ParseSignedRationals(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []SignedRational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeSignedRational.Size() * count) {
log.Panic(ErrNotEnoughData)
}
b := bytes.NewBuffer(data)
value = make([]SignedRational, count)
for i := 0; i < count; i++ {
err = binary.Read(b, byteOrder, &value[i].Numerator)
log.PanicIf(err)
err = binary.Read(b, byteOrder, &value[i].Denominator)
log.PanicIf(err)
}
return value, nil
}

View file

@ -1,397 +0,0 @@
package exif
// NOTE(dustin): Most of this file encapsulates deprecated functionality and awaits being dumped in a future release.
import (
"fmt"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
type TagType struct {
tagType TagTypePrimitive
name string
byteOrder binary.ByteOrder
}
func NewTagType(tagType TagTypePrimitive, byteOrder binary.ByteOrder) TagType {
name, found := TypeNames[tagType]
if found == false {
log.Panicf("tag-type not valid: 0x%04x", tagType)
}
return TagType{
tagType: tagType,
name: name,
byteOrder: byteOrder,
}
}
func (tt TagType) String() string {
return fmt.Sprintf("TagType<NAME=[%s]>", tt.name)
}
func (tt TagType) Name() string {
return tt.name
}
func (tt TagType) Type() TagTypePrimitive {
return tt.tagType
}
func (tt TagType) ByteOrder() binary.ByteOrder {
return tt.byteOrder
}
func (tt TagType) Size() int {
// DEPRECATED(dustin): `(TagTypePrimitive).Size()` should be used, directly.
return tt.Type().Size()
}
// valueIsEmbedded will return a boolean indicating whether the value should be
// found directly within the IFD entry or an offset to somewhere else.
func (tt TagType) valueIsEmbedded(unitCount uint32) bool {
return (tt.tagType.Size() * int(unitCount)) <= 4
}
func (tt TagType) readRawEncoded(valueContext ValueContext) (rawBytes []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
unitSizeRaw := uint32(tt.tagType.Size())
if tt.valueIsEmbedded(valueContext.UnitCount()) == true {
byteLength := unitSizeRaw * valueContext.UnitCount()
return valueContext.RawValueOffset()[:byteLength], nil
} else {
return valueContext.AddressableData()[valueContext.ValueOffset() : valueContext.ValueOffset()+valueContext.UnitCount()*unitSizeRaw], nil
}
}
func (tt TagType) ParseBytes(data []byte, unitCount uint32) (value []uint8, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseBytes()` should be used.
value, err = parser.ParseBytes(data, unitCount)
log.PanicIf(err)
return value, nil
}
// ParseAscii returns a string and auto-strips the trailing NUL character.
func (tt TagType) ParseAscii(data []byte, unitCount uint32) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseAscii()` should be used.
value, err = parser.ParseAscii(data, unitCount)
log.PanicIf(err)
return value, nil
}
// ParseAsciiNoNul returns a string without any consideration for a trailing NUL
// character.
func (tt TagType) ParseAsciiNoNul(data []byte, unitCount uint32) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseAsciiNoNul()` should be used.
value, err = parser.ParseAsciiNoNul(data, unitCount)
log.PanicIf(err)
return value, nil
}
func (tt TagType) ParseShorts(data []byte, unitCount uint32) (value []uint16, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseShorts()` should be used.
value, err = parser.ParseShorts(data, unitCount, tt.byteOrder)
log.PanicIf(err)
return value, nil
}
func (tt TagType) ParseLongs(data []byte, unitCount uint32) (value []uint32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseLongs()` should be used.
value, err = parser.ParseLongs(data, unitCount, tt.byteOrder)
log.PanicIf(err)
return value, nil
}
func (tt TagType) ParseRationals(data []byte, unitCount uint32) (value []Rational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseRationals()` should be used.
value, err = parser.ParseRationals(data, unitCount, tt.byteOrder)
log.PanicIf(err)
return value, nil
}
func (tt TagType) ParseSignedLongs(data []byte, unitCount uint32) (value []int32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseSignedLongs()` should be used.
value, err = parser.ParseSignedLongs(data, unitCount, tt.byteOrder)
log.PanicIf(err)
return value, nil
}
func (tt TagType) ParseSignedRationals(data []byte, unitCount uint32) (value []SignedRational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseSignedRationals()` should be used.
value, err = parser.ParseSignedRationals(data, unitCount, tt.byteOrder)
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadByteValues(valueContext ValueContext) (value []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadBytes()` should be used.
value, err = valueContext.ReadBytes()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadAsciiValue(valueContext ValueContext) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadAscii()` should be used.
value, err = valueContext.ReadAscii()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadAsciiNoNulValue(valueContext ValueContext) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadAsciiNoNul()` should be used.
value, err = valueContext.ReadAsciiNoNul()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadShortValues(valueContext ValueContext) (value []uint16, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadShorts()` should be used.
value, err = valueContext.ReadShorts()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadLongValues(valueContext ValueContext) (value []uint32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadLongs()` should be used.
value, err = valueContext.ReadLongs()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadRationalValues(valueContext ValueContext) (value []Rational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadRationals()` should be used.
value, err = valueContext.ReadRationals()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadSignedLongValues(valueContext ValueContext) (value []int32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadSignedLongs()` should be used.
value, err = valueContext.ReadSignedLongs()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadSignedRationalValues(valueContext ValueContext) (value []SignedRational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadSignedRationals()` should be used.
value, err = valueContext.ReadSignedRationals()
log.PanicIf(err)
return value, nil
}
// ResolveAsString resolves the given value and returns a flat string.
//
// Where the type is not ASCII, `justFirst` indicates whether to just stringify
// the first item in the slice (or return an empty string if the slice is
// empty).
//
// Since this method lacks the information to process unknown-type tags (e.g.
// byte-order, tag-ID, IFD type), it will return an error if attempted. See
// `Undefined()`.
func (tt TagType) ResolveAsString(valueContext ValueContext, justFirst bool) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if justFirst == true {
value, err = valueContext.FormatFirst()
log.PanicIf(err)
} else {
value, err = valueContext.Format()
log.PanicIf(err)
}
return value, nil
}
// Resolve knows how to resolve the given value.
//
// Since this method lacks the information to process unknown-type tags (e.g.
// byte-order, tag-ID, IFD type), it will return an error if attempted. See
// `Undefined()`.
func (tt TagType) Resolve(valueContext *ValueContext) (values interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).Values()` should be used.
values, err = valueContext.Values()
log.PanicIf(err)
return values, nil
}
// Encode knows how to encode the given value to a byte slice.
func (tt TagType) Encode(value interface{}) (encoded []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ve := NewValueEncoder(tt.byteOrder)
ed, err := ve.EncodeWithType(tt, value)
log.PanicIf(err)
return ed.Encoded, err
}
func (tt TagType) FromString(valueString string) (value interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `EncodeStringToBytes()` should be used.
value, err = EncodeStringToBytes(tt.tagType, valueString)
log.PanicIf(err)
return value, nil
}

View file

@ -1,229 +0,0 @@
package exif
import (
"fmt"
"github.com/dsoprea/go-logging"
"gopkg.in/yaml.v2"
)
const (
// IFD1
ThumbnailOffsetTagId = 0x0201
ThumbnailSizeTagId = 0x0202
// Exif
TagVersionId = 0x0000
TagLatitudeId = 0x0002
TagLatitudeRefId = 0x0001
TagLongitudeId = 0x0004
TagLongitudeRefId = 0x0003
TagTimestampId = 0x0007
TagDatestampId = 0x001d
TagAltitudeId = 0x0006
TagAltitudeRefId = 0x0005
)
var (
// tagsWithoutAlignment is a tag-lookup for tags whose value size won't
// necessarily be a multiple of its tag-type.
tagsWithoutAlignment = map[uint16]struct{}{
// The thumbnail offset is stored as a long, but its data is a binary
// blob (not a slice of longs).
ThumbnailOffsetTagId: {},
}
)
var (
tagsLogger = log.NewLogger("exif.tags")
)
// File structures.
type encodedTag struct {
// id is signed, here, because YAML doesn't have enough information to
// support unsigned.
Id int `yaml:"id"`
Name string `yaml:"name"`
TypeName string `yaml:"type_name"`
}
// Indexing structures.
type IndexedTag struct {
Id uint16
Name string
IfdPath string
Type TagTypePrimitive
}
func (it *IndexedTag) String() string {
return fmt.Sprintf("TAG<ID=(0x%04x) NAME=[%s] IFD=[%s]>", it.Id, it.Name, it.IfdPath)
}
func (it *IndexedTag) IsName(ifdPath, name string) bool {
return it.Name == name && it.IfdPath == ifdPath
}
func (it *IndexedTag) Is(ifdPath string, id uint16) bool {
return it.Id == id && it.IfdPath == ifdPath
}
type TagIndex struct {
tagsByIfd map[string]map[uint16]*IndexedTag
tagsByIfdR map[string]map[string]*IndexedTag
}
func NewTagIndex() *TagIndex {
ti := new(TagIndex)
ti.tagsByIfd = make(map[string]map[uint16]*IndexedTag)
ti.tagsByIfdR = make(map[string]map[string]*IndexedTag)
return ti
}
func (ti *TagIndex) Add(it *IndexedTag) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// Store by ID.
family, found := ti.tagsByIfd[it.IfdPath]
if found == false {
family = make(map[uint16]*IndexedTag)
ti.tagsByIfd[it.IfdPath] = family
}
if _, found := family[it.Id]; found == true {
log.Panicf("tag-ID defined more than once for IFD [%s]: (%02x)", it.IfdPath, it.Id)
}
family[it.Id] = it
// Store by name.
familyR, found := ti.tagsByIfdR[it.IfdPath]
if found == false {
familyR = make(map[string]*IndexedTag)
ti.tagsByIfdR[it.IfdPath] = familyR
}
if _, found := familyR[it.Name]; found == true {
log.Panicf("tag-name defined more than once for IFD [%s]: (%s)", it.IfdPath, it.Name)
}
familyR[it.Name] = it
return nil
}
// Get returns information about the non-IFD tag.
func (ti *TagIndex) Get(ifdPath string, id uint16) (it *IndexedTag, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if len(ti.tagsByIfd) == 0 {
err := LoadStandardTags(ti)
log.PanicIf(err)
}
family, found := ti.tagsByIfd[ifdPath]
if found == false {
log.Panic(ErrTagNotFound)
}
it, found = family[id]
if found == false {
log.Panic(ErrTagNotFound)
}
return it, nil
}
// Get returns information about the non-IFD tag.
func (ti *TagIndex) GetWithName(ifdPath string, name string) (it *IndexedTag, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if len(ti.tagsByIfdR) == 0 {
err := LoadStandardTags(ti)
log.PanicIf(err)
}
it, found := ti.tagsByIfdR[ifdPath][name]
if found != true {
log.Panic(ErrTagNotFound)
}
return it, nil
}
// LoadStandardTags registers the tags that all devices/applications should
// support.
func LoadStandardTags(ti *TagIndex) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// Read static data.
encodedIfds := make(map[string][]encodedTag)
err = yaml.Unmarshal([]byte(tagsYaml), encodedIfds)
log.PanicIf(err)
// Load structure.
count := 0
for ifdPath, tags := range encodedIfds {
for _, tagInfo := range tags {
tagId := uint16(tagInfo.Id)
tagName := tagInfo.Name
tagTypeName := tagInfo.TypeName
// TODO(dustin): !! Non-standard types, but found in real data. Ignore for right now.
if tagTypeName == "SSHORT" || tagTypeName == "FLOAT" || tagTypeName == "DOUBLE" {
continue
}
tagTypeId, found := TypeNamesR[tagTypeName]
if found == false {
log.Panicf("type [%s] for [%s] not valid", tagTypeName, tagName)
continue
}
it := &IndexedTag{
IfdPath: ifdPath,
Id: tagId,
Name: tagName,
Type: tagTypeId,
}
err = ti.Add(it)
log.PanicIf(err)
count++
}
}
tagsLogger.Debugf(nil, "(%d) tags loaded.", count)
return nil
}

View file

@ -1,951 +0,0 @@
package exif
var (
// From assets/tags.yaml . Needs to be here so it's embedded in the binary.
tagsYaml = `
# Notes:
#
# This file was produced from http://www.exiv2.org/tags.html, using the included
# tool, though that document appears to have some duplicates when all IDs are
# supposed to be unique (EXIF information only has IDs, not IFDs; IFDs are
# determined by our pre-existing knowledge of those tags).
#
# The webpage that we've produced this file from appears to indicate that
# ImageWidth is represented by both 0x0100 and 0x0001 depending on whether the
# encoding is RGB or YCbCr.
IFD/Exif:
- id: 0x829a
name: ExposureTime
type_name: RATIONAL
- id: 0x829d
name: FNumber
type_name: RATIONAL
- id: 0x8822
name: ExposureProgram
type_name: SHORT
- id: 0x8824
name: SpectralSensitivity
type_name: ASCII
- id: 0x8827
name: ISOSpeedRatings
type_name: SHORT
- id: 0x8828
name: OECF
type_name: UNDEFINED
- id: 0x8830
name: SensitivityType
type_name: SHORT
- id: 0x8831
name: StandardOutputSensitivity
type_name: LONG
- id: 0x8832
name: RecommendedExposureIndex
type_name: LONG
- id: 0x8833
name: ISOSpeed
type_name: LONG
- id: 0x8834
name: ISOSpeedLatitudeyyy
type_name: LONG
- id: 0x8835
name: ISOSpeedLatitudezzz
type_name: LONG
- id: 0x9000
name: ExifVersion
type_name: UNDEFINED
- id: 0x9003
name: DateTimeOriginal
type_name: ASCII
- id: 0x9004
name: DateTimeDigitized
type_name: ASCII
- id: 0x9101
name: ComponentsConfiguration
type_name: UNDEFINED
- id: 0x9102
name: CompressedBitsPerPixel
type_name: RATIONAL
- id: 0x9201
name: ShutterSpeedValue
type_name: SRATIONAL
- id: 0x9202
name: ApertureValue
type_name: RATIONAL
- id: 0x9203
name: BrightnessValue
type_name: SRATIONAL
- id: 0x9204
name: ExposureBiasValue
type_name: SRATIONAL
- id: 0x9205
name: MaxApertureValue
type_name: RATIONAL
- id: 0x9206
name: SubjectDistance
type_name: RATIONAL
- id: 0x9207
name: MeteringMode
type_name: SHORT
- id: 0x9208
name: LightSource
type_name: SHORT
- id: 0x9209
name: Flash
type_name: SHORT
- id: 0x920a
name: FocalLength
type_name: RATIONAL
- id: 0x9214
name: SubjectArea
type_name: SHORT
- id: 0x927c
name: MakerNote
type_name: UNDEFINED
- id: 0x9286
name: UserComment
type_name: UNDEFINED
- id: 0x9290
name: SubSecTime
type_name: ASCII
- id: 0x9291
name: SubSecTimeOriginal
type_name: ASCII
- id: 0x9292
name: SubSecTimeDigitized
type_name: ASCII
- id: 0xa000
name: FlashpixVersion
type_name: UNDEFINED
- id: 0xa001
name: ColorSpace
type_name: SHORT
- id: 0xa002
name: PixelXDimension
type_name: LONG
- id: 0xa003
name: PixelYDimension
type_name: LONG
- id: 0xa004
name: RelatedSoundFile
type_name: ASCII
- id: 0xa005
name: InteroperabilityTag
type_name: LONG
- id: 0xa20b
name: FlashEnergy
type_name: RATIONAL
- id: 0xa20c
name: SpatialFrequencyResponse
type_name: UNDEFINED
- id: 0xa20e
name: FocalPlaneXResolution
type_name: RATIONAL
- id: 0xa20f
name: FocalPlaneYResolution
type_name: RATIONAL
- id: 0xa210
name: FocalPlaneResolutionUnit
type_name: SHORT
- id: 0xa214
name: SubjectLocation
type_name: SHORT
- id: 0xa215
name: ExposureIndex
type_name: RATIONAL
- id: 0xa217
name: SensingMethod
type_name: SHORT
- id: 0xa300
name: FileSource
type_name: UNDEFINED
- id: 0xa301
name: SceneType
type_name: UNDEFINED
- id: 0xa302
name: CFAPattern
type_name: UNDEFINED
- id: 0xa401
name: CustomRendered
type_name: SHORT
- id: 0xa402
name: ExposureMode
type_name: SHORT
- id: 0xa403
name: WhiteBalance
type_name: SHORT
- id: 0xa404
name: DigitalZoomRatio
type_name: RATIONAL
- id: 0xa405
name: FocalLengthIn35mmFilm
type_name: SHORT
- id: 0xa406
name: SceneCaptureType
type_name: SHORT
- id: 0xa407
name: GainControl
type_name: SHORT
- id: 0xa408
name: Contrast
type_name: SHORT
- id: 0xa409
name: Saturation
type_name: SHORT
- id: 0xa40a
name: Sharpness
type_name: SHORT
- id: 0xa40b
name: DeviceSettingDescription
type_name: UNDEFINED
- id: 0xa40c
name: SubjectDistanceRange
type_name: SHORT
- id: 0xa420
name: ImageUniqueID
type_name: ASCII
- id: 0xa430
name: CameraOwnerName
type_name: ASCII
- id: 0xa431
name: BodySerialNumber
type_name: ASCII
- id: 0xa432
name: LensSpecification
type_name: RATIONAL
- id: 0xa433
name: LensMake
type_name: ASCII
- id: 0xa434
name: LensModel
type_name: ASCII
- id: 0xa435
name: LensSerialNumber
type_name: ASCII
IFD/GPSInfo:
- id: 0x0000
name: GPSVersionID
type_name: BYTE
- id: 0x0001
name: GPSLatitudeRef
type_name: ASCII
- id: 0x0002
name: GPSLatitude
type_name: RATIONAL
- id: 0x0003
name: GPSLongitudeRef
type_name: ASCII
- id: 0x0004
name: GPSLongitude
type_name: RATIONAL
- id: 0x0005
name: GPSAltitudeRef
type_name: BYTE
- id: 0x0006
name: GPSAltitude
type_name: RATIONAL
- id: 0x0007
name: GPSTimeStamp
type_name: RATIONAL
- id: 0x0008
name: GPSSatellites
type_name: ASCII
- id: 0x0009
name: GPSStatus
type_name: ASCII
- id: 0x000a
name: GPSMeasureMode
type_name: ASCII
- id: 0x000b
name: GPSDOP
type_name: RATIONAL
- id: 0x000c
name: GPSSpeedRef
type_name: ASCII
- id: 0x000d
name: GPSSpeed
type_name: RATIONAL
- id: 0x000e
name: GPSTrackRef
type_name: ASCII
- id: 0x000f
name: GPSTrack
type_name: RATIONAL
- id: 0x0010
name: GPSImgDirectionRef
type_name: ASCII
- id: 0x0011
name: GPSImgDirection
type_name: RATIONAL
- id: 0x0012
name: GPSMapDatum
type_name: ASCII
- id: 0x0013
name: GPSDestLatitudeRef
type_name: ASCII
- id: 0x0014
name: GPSDestLatitude
type_name: RATIONAL
- id: 0x0015
name: GPSDestLongitudeRef
type_name: ASCII
- id: 0x0016
name: GPSDestLongitude
type_name: RATIONAL
- id: 0x0017
name: GPSDestBearingRef
type_name: ASCII
- id: 0x0018
name: GPSDestBearing
type_name: RATIONAL
- id: 0x0019
name: GPSDestDistanceRef
type_name: ASCII
- id: 0x001a
name: GPSDestDistance
type_name: RATIONAL
- id: 0x001b
name: GPSProcessingMethod
type_name: UNDEFINED
- id: 0x001c
name: GPSAreaInformation
type_name: UNDEFINED
- id: 0x001d
name: GPSDateStamp
type_name: ASCII
- id: 0x001e
name: GPSDifferential
type_name: SHORT
IFD:
- id: 0x000b
name: ProcessingSoftware
type_name: ASCII
- id: 0x00fe
name: NewSubfileType
type_name: LONG
- id: 0x00ff
name: SubfileType
type_name: SHORT
- id: 0x0100
name: ImageWidth
type_name: LONG
- id: 0x0101
name: ImageLength
type_name: LONG
- id: 0x0102
name: BitsPerSample
type_name: SHORT
- id: 0x0103
name: Compression
type_name: SHORT
- id: 0x0106
name: PhotometricInterpretation
type_name: SHORT
- id: 0x0107
name: Thresholding
type_name: SHORT
- id: 0x0108
name: CellWidth
type_name: SHORT
- id: 0x0109
name: CellLength
type_name: SHORT
- id: 0x010a
name: FillOrder
type_name: SHORT
- id: 0x010d
name: DocumentName
type_name: ASCII
- id: 0x010e
name: ImageDescription
type_name: ASCII
- id: 0x010f
name: Make
type_name: ASCII
- id: 0x0110
name: Model
type_name: ASCII
- id: 0x0111
name: StripOffsets
type_name: LONG
- id: 0x0112
name: Orientation
type_name: SHORT
- id: 0x0115
name: SamplesPerPixel
type_name: SHORT
- id: 0x0116
name: RowsPerStrip
type_name: LONG
- id: 0x0117
name: StripByteCounts
type_name: LONG
- id: 0x011a
name: XResolution
type_name: RATIONAL
- id: 0x011b
name: YResolution
type_name: RATIONAL
- id: 0x011c
name: PlanarConfiguration
type_name: SHORT
- id: 0x0122
name: GrayResponseUnit
type_name: SHORT
- id: 0x0123
name: GrayResponseCurve
type_name: SHORT
- id: 0x0124
name: T4Options
type_name: LONG
- id: 0x0125
name: T6Options
type_name: LONG
- id: 0x0128
name: ResolutionUnit
type_name: SHORT
- id: 0x0129
name: PageNumber
type_name: SHORT
- id: 0x012d
name: TransferFunction
type_name: SHORT
- id: 0x0131
name: Software
type_name: ASCII
- id: 0x0132
name: DateTime
type_name: ASCII
- id: 0x013b
name: Artist
type_name: ASCII
- id: 0x013c
name: HostComputer
type_name: ASCII
- id: 0x013d
name: Predictor
type_name: SHORT
- id: 0x013e
name: WhitePoint
type_name: RATIONAL
- id: 0x013f
name: PrimaryChromaticities
type_name: RATIONAL
- id: 0x0140
name: ColorMap
type_name: SHORT
- id: 0x0141
name: HalftoneHints
type_name: SHORT
- id: 0x0142
name: TileWidth
type_name: SHORT
- id: 0x0143
name: TileLength
type_name: SHORT
- id: 0x0144
name: TileOffsets
type_name: SHORT
- id: 0x0145
name: TileByteCounts
type_name: SHORT
- id: 0x014a
name: SubIFDs
type_name: LONG
- id: 0x014c
name: InkSet
type_name: SHORT
- id: 0x014d
name: InkNames
type_name: ASCII
- id: 0x014e
name: NumberOfInks
type_name: SHORT
- id: 0x0150
name: DotRange
type_name: BYTE
- id: 0x0151
name: TargetPrinter
type_name: ASCII
- id: 0x0152
name: ExtraSamples
type_name: SHORT
- id: 0x0153
name: SampleFormat
type_name: SHORT
- id: 0x0154
name: SMinSampleValue
type_name: SHORT
- id: 0x0155
name: SMaxSampleValue
type_name: SHORT
- id: 0x0156
name: TransferRange
type_name: SHORT
- id: 0x0157
name: ClipPath
type_name: BYTE
- id: 0x0158
name: XClipPathUnits
type_name: SSHORT
- id: 0x0159
name: YClipPathUnits
type_name: SSHORT
- id: 0x015a
name: Indexed
type_name: SHORT
- id: 0x015b
name: JPEGTables
type_name: UNDEFINED
- id: 0x015f
name: OPIProxy
type_name: SHORT
- id: 0x0200
name: JPEGProc
type_name: LONG
- id: 0x0201
name: JPEGInterchangeFormat
type_name: LONG
- id: 0x0202
name: JPEGInterchangeFormatLength
type_name: LONG
- id: 0x0203
name: JPEGRestartInterval
type_name: SHORT
- id: 0x0205
name: JPEGLosslessPredictors
type_name: SHORT
- id: 0x0206
name: JPEGPointTransforms
type_name: SHORT
- id: 0x0207
name: JPEGQTables
type_name: LONG
- id: 0x0208
name: JPEGDCTables
type_name: LONG
- id: 0x0209
name: JPEGACTables
type_name: LONG
- id: 0x0211
name: YCbCrCoefficients
type_name: RATIONAL
- id: 0x0212
name: YCbCrSubSampling
type_name: SHORT
- id: 0x0213
name: YCbCrPositioning
type_name: SHORT
- id: 0x0214
name: ReferenceBlackWhite
type_name: RATIONAL
- id: 0x02bc
name: XMLPacket
type_name: BYTE
- id: 0x4746
name: Rating
type_name: SHORT
- id: 0x4749
name: RatingPercent
type_name: SHORT
- id: 0x800d
name: ImageID
type_name: ASCII
- id: 0x828d
name: CFARepeatPatternDim
type_name: SHORT
- id: 0x828e
name: CFAPattern
type_name: BYTE
- id: 0x828f
name: BatteryLevel
type_name: RATIONAL
- id: 0x8298
name: Copyright
type_name: ASCII
- id: 0x829a
name: ExposureTime
type_name: RATIONAL
- id: 0x829d
name: FNumber
type_name: RATIONAL
- id: 0x83bb
name: IPTCNAA
type_name: LONG
- id: 0x8649
name: ImageResources
type_name: BYTE
- id: 0x8769
name: ExifTag
type_name: LONG
- id: 0x8773
name: InterColorProfile
type_name: UNDEFINED
- id: 0x8822
name: ExposureProgram
type_name: SHORT
- id: 0x8824
name: SpectralSensitivity
type_name: ASCII
- id: 0x8825
name: GPSTag
type_name: LONG
- id: 0x8827
name: ISOSpeedRatings
type_name: SHORT
- id: 0x8828
name: OECF
type_name: UNDEFINED
- id: 0x8829
name: Interlace
type_name: SHORT
- id: 0x882a
name: TimeZoneOffset
type_name: SSHORT
- id: 0x882b
name: SelfTimerMode
type_name: SHORT
- id: 0x9003
name: DateTimeOriginal
type_name: ASCII
- id: 0x9102
name: CompressedBitsPerPixel
type_name: RATIONAL
- id: 0x9201
name: ShutterSpeedValue
type_name: SRATIONAL
- id: 0x9202
name: ApertureValue
type_name: RATIONAL
- id: 0x9203
name: BrightnessValue
type_name: SRATIONAL
- id: 0x9204
name: ExposureBiasValue
type_name: SRATIONAL
- id: 0x9205
name: MaxApertureValue
type_name: RATIONAL
- id: 0x9206
name: SubjectDistance
type_name: SRATIONAL
- id: 0x9207
name: MeteringMode
type_name: SHORT
- id: 0x9208
name: LightSource
type_name: SHORT
- id: 0x9209
name: Flash
type_name: SHORT
- id: 0x920a
name: FocalLength
type_name: RATIONAL
- id: 0x920b
name: FlashEnergy
type_name: RATIONAL
- id: 0x920c
name: SpatialFrequencyResponse
type_name: UNDEFINED
- id: 0x920d
name: Noise
type_name: UNDEFINED
- id: 0x920e
name: FocalPlaneXResolution
type_name: RATIONAL
- id: 0x920f
name: FocalPlaneYResolution
type_name: RATIONAL
- id: 0x9210
name: FocalPlaneResolutionUnit
type_name: SHORT
- id: 0x9211
name: ImageNumber
type_name: LONG
- id: 0x9212
name: SecurityClassification
type_name: ASCII
- id: 0x9213
name: ImageHistory
type_name: ASCII
- id: 0x9214
name: SubjectLocation
type_name: SHORT
- id: 0x9215
name: ExposureIndex
type_name: RATIONAL
- id: 0x9216
name: TIFFEPStandardID
type_name: BYTE
- id: 0x9217
name: SensingMethod
type_name: SHORT
- id: 0x9c9b
name: XPTitle
type_name: BYTE
- id: 0x9c9c
name: XPComment
type_name: BYTE
- id: 0x9c9d
name: XPAuthor
type_name: BYTE
- id: 0x9c9e
name: XPKeywords
type_name: BYTE
- id: 0x9c9f
name: XPSubject
type_name: BYTE
- id: 0xc4a5
name: PrintImageMatching
type_name: UNDEFINED
- id: 0xc612
name: DNGVersion
type_name: BYTE
- id: 0xc613
name: DNGBackwardVersion
type_name: BYTE
- id: 0xc614
name: UniqueCameraModel
type_name: ASCII
- id: 0xc615
name: LocalizedCameraModel
type_name: BYTE
- id: 0xc616
name: CFAPlaneColor
type_name: BYTE
- id: 0xc617
name: CFALayout
type_name: SHORT
- id: 0xc618
name: LinearizationTable
type_name: SHORT
- id: 0xc619
name: BlackLevelRepeatDim
type_name: SHORT
- id: 0xc61a
name: BlackLevel
type_name: RATIONAL
- id: 0xc61b
name: BlackLevelDeltaH
type_name: SRATIONAL
- id: 0xc61c
name: BlackLevelDeltaV
type_name: SRATIONAL
- id: 0xc61d
name: WhiteLevel
type_name: SHORT
- id: 0xc61e
name: DefaultScale
type_name: RATIONAL
- id: 0xc61f
name: DefaultCropOrigin
type_name: SHORT
- id: 0xc620
name: DefaultCropSize
type_name: SHORT
- id: 0xc621
name: ColorMatrix1
type_name: SRATIONAL
- id: 0xc622
name: ColorMatrix2
type_name: SRATIONAL
- id: 0xc623
name: CameraCalibration1
type_name: SRATIONAL
- id: 0xc624
name: CameraCalibration2
type_name: SRATIONAL
- id: 0xc625
name: ReductionMatrix1
type_name: SRATIONAL
- id: 0xc626
name: ReductionMatrix2
type_name: SRATIONAL
- id: 0xc627
name: AnalogBalance
type_name: RATIONAL
- id: 0xc628
name: AsShotNeutral
type_name: SHORT
- id: 0xc629
name: AsShotWhiteXY
type_name: RATIONAL
- id: 0xc62a
name: BaselineExposure
type_name: SRATIONAL
- id: 0xc62b
name: BaselineNoise
type_name: RATIONAL
- id: 0xc62c
name: BaselineSharpness
type_name: RATIONAL
- id: 0xc62d
name: BayerGreenSplit
type_name: LONG
- id: 0xc62e
name: LinearResponseLimit
type_name: RATIONAL
- id: 0xc62f
name: CameraSerialNumber
type_name: ASCII
- id: 0xc630
name: LensInfo
type_name: RATIONAL
- id: 0xc631
name: ChromaBlurRadius
type_name: RATIONAL
- id: 0xc632
name: AntiAliasStrength
type_name: RATIONAL
- id: 0xc633
name: ShadowScale
type_name: SRATIONAL
- id: 0xc634
name: DNGPrivateData
type_name: BYTE
- id: 0xc635
name: MakerNoteSafety
type_name: SHORT
- id: 0xc65a
name: CalibrationIlluminant1
type_name: SHORT
- id: 0xc65b
name: CalibrationIlluminant2
type_name: SHORT
- id: 0xc65c
name: BestQualityScale
type_name: RATIONAL
- id: 0xc65d
name: RawDataUniqueID
type_name: BYTE
- id: 0xc68b
name: OriginalRawFileName
type_name: BYTE
- id: 0xc68c
name: OriginalRawFileData
type_name: UNDEFINED
- id: 0xc68d
name: ActiveArea
type_name: SHORT
- id: 0xc68e
name: MaskedAreas
type_name: SHORT
- id: 0xc68f
name: AsShotICCProfile
type_name: UNDEFINED
- id: 0xc690
name: AsShotPreProfileMatrix
type_name: SRATIONAL
- id: 0xc691
name: CurrentICCProfile
type_name: UNDEFINED
- id: 0xc692
name: CurrentPreProfileMatrix
type_name: SRATIONAL
- id: 0xc6bf
name: ColorimetricReference
type_name: SHORT
- id: 0xc6f3
name: CameraCalibrationSignature
type_name: BYTE
- id: 0xc6f4
name: ProfileCalibrationSignature
type_name: BYTE
- id: 0xc6f6
name: AsShotProfileName
type_name: BYTE
- id: 0xc6f7
name: NoiseReductionApplied
type_name: RATIONAL
- id: 0xc6f8
name: ProfileName
type_name: BYTE
- id: 0xc6f9
name: ProfileHueSatMapDims
type_name: LONG
- id: 0xc6fa
name: ProfileHueSatMapData1
type_name: FLOAT
- id: 0xc6fb
name: ProfileHueSatMapData2
type_name: FLOAT
- id: 0xc6fc
name: ProfileToneCurve
type_name: FLOAT
- id: 0xc6fd
name: ProfileEmbedPolicy
type_name: LONG
- id: 0xc6fe
name: ProfileCopyright
type_name: BYTE
- id: 0xc714
name: ForwardMatrix1
type_name: SRATIONAL
- id: 0xc715
name: ForwardMatrix2
type_name: SRATIONAL
- id: 0xc716
name: PreviewApplicationName
type_name: BYTE
- id: 0xc717
name: PreviewApplicationVersion
type_name: BYTE
- id: 0xc718
name: PreviewSettingsName
type_name: BYTE
- id: 0xc719
name: PreviewSettingsDigest
type_name: BYTE
- id: 0xc71a
name: PreviewColorSpace
type_name: LONG
- id: 0xc71b
name: PreviewDateTime
type_name: ASCII
- id: 0xc71c
name: RawImageDigest
type_name: UNDEFINED
- id: 0xc71d
name: OriginalRawFileDigest
type_name: UNDEFINED
- id: 0xc71e
name: SubTileBlockSize
type_name: LONG
- id: 0xc71f
name: RowInterleaveFactor
type_name: LONG
- id: 0xc725
name: ProfileLookTableDims
type_name: LONG
- id: 0xc726
name: ProfileLookTableData
type_name: FLOAT
- id: 0xc740
name: OpcodeList1
type_name: UNDEFINED
- id: 0xc741
name: OpcodeList2
type_name: UNDEFINED
- id: 0xc74e
name: OpcodeList3
type_name: UNDEFINED
- id: 0xc761
name: NoiseProfile
type_name: DOUBLE
IFD/Exif/Iop:
- id: 0x0001
name: InteroperabilityIndex
type_name: ASCII
- id: 0x0002
name: InteroperabilityVersion
type_name: UNDEFINED
- id: 0x1000
name: RelatedImageFileFormat
type_name: ASCII
- id: 0x1001
name: RelatedImageWidth
type_name: LONG
- id: 0x1002
name: RelatedImageLength
type_name: LONG
`
)

View file

@ -1,417 +0,0 @@
package exif
import (
"bytes"
"fmt"
"strings"
"crypto/sha1"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
const (
UnparseableUnknownTagValuePlaceholder = "!UNKNOWN"
)
// TODO(dustin): Rename "unknown" in symbol names to "undefined" in the next release.
//
// See https://github.com/dsoprea/go-exif/issues/27 .
const (
TagUnknownType_9298_UserComment_Encoding_ASCII = iota
TagUnknownType_9298_UserComment_Encoding_JIS = iota
TagUnknownType_9298_UserComment_Encoding_UNICODE = iota
TagUnknownType_9298_UserComment_Encoding_UNDEFINED = iota
)
const (
TagUnknownType_9101_ComponentsConfiguration_Channel_Y = 0x1
TagUnknownType_9101_ComponentsConfiguration_Channel_Cb = 0x2
TagUnknownType_9101_ComponentsConfiguration_Channel_Cr = 0x3
TagUnknownType_9101_ComponentsConfiguration_Channel_R = 0x4
TagUnknownType_9101_ComponentsConfiguration_Channel_G = 0x5
TagUnknownType_9101_ComponentsConfiguration_Channel_B = 0x6
)
const (
TagUnknownType_9101_ComponentsConfiguration_OTHER = iota
TagUnknownType_9101_ComponentsConfiguration_RGB = iota
TagUnknownType_9101_ComponentsConfiguration_YCBCR = iota
)
var (
TagUnknownType_9298_UserComment_Encoding_Names = map[int]string{
TagUnknownType_9298_UserComment_Encoding_ASCII: "ASCII",
TagUnknownType_9298_UserComment_Encoding_JIS: "JIS",
TagUnknownType_9298_UserComment_Encoding_UNICODE: "UNICODE",
TagUnknownType_9298_UserComment_Encoding_UNDEFINED: "UNDEFINED",
}
TagUnknownType_9298_UserComment_Encodings = map[int][]byte{
TagUnknownType_9298_UserComment_Encoding_ASCII: {'A', 'S', 'C', 'I', 'I', 0, 0, 0},
TagUnknownType_9298_UserComment_Encoding_JIS: {'J', 'I', 'S', 0, 0, 0, 0, 0},
TagUnknownType_9298_UserComment_Encoding_UNICODE: {'U', 'n', 'i', 'c', 'o', 'd', 'e', 0},
TagUnknownType_9298_UserComment_Encoding_UNDEFINED: {0, 0, 0, 0, 0, 0, 0, 0},
}
TagUnknownType_9101_ComponentsConfiguration_Names = map[int]string{
TagUnknownType_9101_ComponentsConfiguration_OTHER: "OTHER",
TagUnknownType_9101_ComponentsConfiguration_RGB: "RGB",
TagUnknownType_9101_ComponentsConfiguration_YCBCR: "YCBCR",
}
TagUnknownType_9101_ComponentsConfiguration_Configurations = map[int][]byte{
TagUnknownType_9101_ComponentsConfiguration_RGB: {
TagUnknownType_9101_ComponentsConfiguration_Channel_R,
TagUnknownType_9101_ComponentsConfiguration_Channel_G,
TagUnknownType_9101_ComponentsConfiguration_Channel_B,
0,
},
TagUnknownType_9101_ComponentsConfiguration_YCBCR: {
TagUnknownType_9101_ComponentsConfiguration_Channel_Y,
TagUnknownType_9101_ComponentsConfiguration_Channel_Cb,
TagUnknownType_9101_ComponentsConfiguration_Channel_Cr,
0,
},
}
)
// TODO(dustin): Rename `UnknownTagValue` to `UndefinedTagValue`.
type UnknownTagValue interface {
ValueBytes() ([]byte, error)
}
// TODO(dustin): Rename `TagUnknownType_GeneralString` to `TagUnknownType_GeneralString`.
type TagUnknownType_GeneralString string
func (gs TagUnknownType_GeneralString) ValueBytes() (value []byte, err error) {
return []byte(gs), nil
}
// TODO(dustin): Rename `TagUnknownType_9298_UserComment` to `TagUndefinedType_9298_UserComment`.
type TagUnknownType_9298_UserComment struct {
EncodingType int
EncodingBytes []byte
}
func (uc TagUnknownType_9298_UserComment) String() string {
var valuePhrase string
if len(uc.EncodingBytes) <= 8 {
valuePhrase = fmt.Sprintf("%v", uc.EncodingBytes)
} else {
valuePhrase = fmt.Sprintf("%v...", uc.EncodingBytes[:8])
}
return fmt.Sprintf("UserComment<SIZE=(%d) ENCODING=[%s] V=%v LEN=(%d)>", len(uc.EncodingBytes), TagUnknownType_9298_UserComment_Encoding_Names[uc.EncodingType], valuePhrase, len(uc.EncodingBytes))
}
func (uc TagUnknownType_9298_UserComment) ValueBytes() (value []byte, err error) {
encodingTypeBytes, found := TagUnknownType_9298_UserComment_Encodings[uc.EncodingType]
if found == false {
log.Panicf("encoding-type not valid for unknown-type tag 9298 (UserComment): (%d)", uc.EncodingType)
}
value = make([]byte, len(uc.EncodingBytes)+8)
copy(value[:8], encodingTypeBytes)
copy(value[8:], uc.EncodingBytes)
return value, nil
}
// TODO(dustin): Rename `TagUnknownType_927C_MakerNote` to `TagUndefinedType_927C_MakerNote`.
type TagUnknownType_927C_MakerNote struct {
MakerNoteType []byte
MakerNoteBytes []byte
}
func (mn TagUnknownType_927C_MakerNote) String() string {
parts := make([]string, 20)
for i, c := range mn.MakerNoteType {
parts[i] = fmt.Sprintf("%02x", c)
}
h := sha1.New()
_, err := h.Write(mn.MakerNoteBytes)
log.PanicIf(err)
digest := h.Sum(nil)
return fmt.Sprintf("MakerNote<TYPE-ID=[%s] LEN=(%d) SHA1=[%020x]>", strings.Join(parts, " "), len(mn.MakerNoteBytes), digest)
}
func (uc TagUnknownType_927C_MakerNote) ValueBytes() (value []byte, err error) {
return uc.MakerNoteBytes, nil
}
// TODO(dustin): Rename `TagUnknownType_9101_ComponentsConfiguration` to `TagUndefinedType_9101_ComponentsConfiguration`.
type TagUnknownType_9101_ComponentsConfiguration struct {
ConfigurationId int
ConfigurationBytes []byte
}
func (cc TagUnknownType_9101_ComponentsConfiguration) String() string {
return fmt.Sprintf("ComponentsConfiguration<ID=[%s] BYTES=%v>", TagUnknownType_9101_ComponentsConfiguration_Names[cc.ConfigurationId], cc.ConfigurationBytes)
}
func (uc TagUnknownType_9101_ComponentsConfiguration) ValueBytes() (value []byte, err error) {
return uc.ConfigurationBytes, nil
}
// TODO(dustin): Rename `EncodeUnknown_9286` to `EncodeUndefined_9286`.
func EncodeUnknown_9286(uc TagUnknownType_9298_UserComment) (encoded []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
b := new(bytes.Buffer)
encodingTypeBytes := TagUnknownType_9298_UserComment_Encodings[uc.EncodingType]
_, err = b.Write(encodingTypeBytes)
log.PanicIf(err)
_, err = b.Write(uc.EncodingBytes)
log.PanicIf(err)
return b.Bytes(), nil
}
type EncodeableUndefinedValue struct {
IfdPath string
TagId uint16
Parameters interface{}
}
func EncodeUndefined(ifdPath string, tagId uint16, value interface{}) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): !! Finish implementing these.
if ifdPath == IfdPathStandardExif {
if tagId == 0x9286 {
encoded, err := EncodeUnknown_9286(value.(TagUnknownType_9298_UserComment))
log.PanicIf(err)
ed.Type = TypeUndefined
ed.Encoded = encoded
ed.UnitCount = uint32(len(encoded))
return ed, nil
}
}
log.Panicf("undefined value not encodable: %s (0x%02x)", ifdPath, tagId)
// Never called.
return EncodedData{}, nil
}
// TODO(dustin): Rename `TagUnknownType_UnknownValue` to `TagUndefinedType_UnknownValue`.
type TagUnknownType_UnknownValue []byte
func (tutuv TagUnknownType_UnknownValue) String() string {
parts := make([]string, len(tutuv))
for i, c := range tutuv {
parts[i] = fmt.Sprintf("%02x", c)
}
h := sha1.New()
_, err := h.Write(tutuv)
log.PanicIf(err)
digest := h.Sum(nil)
return fmt.Sprintf("Unknown<DATA=[%s] LEN=(%d) SHA1=[%020x]>", strings.Join(parts, " "), len(tutuv), digest)
}
// UndefinedValue knows how to resolve the value for most unknown-type tags.
func UndefinedValue(ifdPath string, tagId uint16, valueContext interface{}, byteOrder binary.ByteOrder) (value interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): Stop exporting this. Use `(*ValueContext).Undefined()`.
var valueContextPtr *ValueContext
if vc, ok := valueContext.(*ValueContext); ok == true {
// Legacy usage.
valueContextPtr = vc
} else {
// Standard usage.
valueContextValue := valueContext.(ValueContext)
valueContextPtr = &valueContextValue
}
typeLogger.Debugf(nil, "UndefinedValue: IFD-PATH=[%s] TAG-ID=(0x%02x)", ifdPath, tagId)
if ifdPath == IfdPathStandardExif {
if tagId == 0x9000 {
// ExifVersion
valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
valueString, err := valueContextPtr.ReadAsciiNoNul()
log.PanicIf(err)
return TagUnknownType_GeneralString(valueString), nil
} else if tagId == 0xa000 {
// FlashpixVersion
valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
valueString, err := valueContextPtr.ReadAsciiNoNul()
log.PanicIf(err)
return TagUnknownType_GeneralString(valueString), nil
} else if tagId == 0x9286 {
// UserComment
valueContextPtr.SetUnknownValueType(TypeByte)
valueBytes, err := valueContextPtr.ReadBytes()
log.PanicIf(err)
unknownUc := TagUnknownType_9298_UserComment{
EncodingType: TagUnknownType_9298_UserComment_Encoding_UNDEFINED,
EncodingBytes: []byte{},
}
encoding := valueBytes[:8]
for encodingIndex, encodingBytes := range TagUnknownType_9298_UserComment_Encodings {
if bytes.Compare(encoding, encodingBytes) == 0 {
uc := TagUnknownType_9298_UserComment{
EncodingType: encodingIndex,
EncodingBytes: valueBytes[8:],
}
return uc, nil
}
}
typeLogger.Warningf(nil, "User-comment encoding not valid. Returning 'unknown' type (the default).")
return unknownUc, nil
} else if tagId == 0x927c {
// MakerNote
// TODO(dustin): !! This is the Wild Wild West. This very well might be a child IFD, but any and all OEM's define their own formats. If we're going to be writing changes and this is complete EXIF (which may not have the first eight bytes), it might be fine. However, if these are just IFDs they'll be relative to the main EXIF, this will invalidate the MakerNote data for IFDs and any other implementations that use offsets unless we can interpret them all. It be best to return to this later and just exclude this from being written for now, though means a loss of a wealth of image metadata.
// -> We can also just blindly try to interpret as an IFD and just validate that it's looks good (maybe it will even have a 'next ifd' pointer that we can validate is 0x0).
valueContextPtr.SetUnknownValueType(TypeByte)
valueBytes, err := valueContextPtr.ReadBytes()
log.PanicIf(err)
// TODO(dustin): Doesn't work, but here as an example.
// ie := NewIfdEnumerate(valueBytes, byteOrder)
// // TODO(dustin): !! Validate types (might have proprietary types, but it might be worth splitting the list between valid and not valid; maybe fail if a certain proportion are invalid, or maybe aren't less then a certain small integer)?
// ii, err := ie.Collect(0x0)
// for _, entry := range ii.RootIfd.Entries {
// fmt.Printf("ENTRY: 0x%02x %d\n", entry.TagId, entry.TagType)
// }
mn := TagUnknownType_927C_MakerNote{
MakerNoteType: valueBytes[:20],
// MakerNoteBytes has the whole length of bytes. There's always
// the chance that the first 20 bytes includes actual data.
MakerNoteBytes: valueBytes,
}
return mn, nil
} else if tagId == 0x9101 {
// ComponentsConfiguration
valueContextPtr.SetUnknownValueType(TypeByte)
valueBytes, err := valueContextPtr.ReadBytes()
log.PanicIf(err)
for configurationId, configurationBytes := range TagUnknownType_9101_ComponentsConfiguration_Configurations {
if bytes.Compare(valueBytes, configurationBytes) == 0 {
cc := TagUnknownType_9101_ComponentsConfiguration{
ConfigurationId: configurationId,
ConfigurationBytes: valueBytes,
}
return cc, nil
}
}
cc := TagUnknownType_9101_ComponentsConfiguration{
ConfigurationId: TagUnknownType_9101_ComponentsConfiguration_OTHER,
ConfigurationBytes: valueBytes,
}
return cc, nil
}
} else if ifdPath == IfdPathStandardGps {
if tagId == 0x001c {
// GPSAreaInformation
valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
valueString, err := valueContextPtr.ReadAsciiNoNul()
log.PanicIf(err)
return TagUnknownType_GeneralString(valueString), nil
} else if tagId == 0x001b {
// GPSProcessingMethod
valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
valueString, err := valueContextPtr.ReadAsciiNoNul()
log.PanicIf(err)
return TagUnknownType_GeneralString(valueString), nil
}
} else if ifdPath == IfdPathStandardExifIop {
if tagId == 0x0002 {
// InteropVersion
valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
valueString, err := valueContextPtr.ReadAsciiNoNul()
log.PanicIf(err)
return TagUnknownType_GeneralString(valueString), nil
}
}
// TODO(dustin): !! Still need to do:
//
// complex: 0xa302, 0xa20c, 0x8828
// long: 0xa301, 0xa300
//
// 0xa40b is device-specific and unhandled.
//
// See https://github.com/dsoprea/go-exif/issues/26.
// We have no choice but to return the error. We have no way of knowing how
// much data there is without already knowing what data-type this tag is.
return nil, ErrUnhandledUnknownTypedTag
}

View file

@ -1,310 +0,0 @@
package exif
import (
"errors"
"fmt"
"strconv"
"strings"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
type TagTypePrimitive uint16
func (typeType TagTypePrimitive) String() string {
return TypeNames[typeType]
}
func (tagType TagTypePrimitive) Size() int {
if tagType == TypeByte {
return 1
} else if tagType == TypeAscii || tagType == TypeAsciiNoNul {
return 1
} else if tagType == TypeShort {
return 2
} else if tagType == TypeLong {
return 4
} else if tagType == TypeRational {
return 8
} else if tagType == TypeSignedLong {
return 4
} else if tagType == TypeSignedRational {
return 8
} else {
log.Panicf("can not determine tag-value size for type (%d): [%s]", tagType, TypeNames[tagType])
// Never called.
return 0
}
}
const (
TypeByte TagTypePrimitive = 1
TypeAscii TagTypePrimitive = 2
TypeShort TagTypePrimitive = 3
TypeLong TagTypePrimitive = 4
TypeRational TagTypePrimitive = 5
TypeUndefined TagTypePrimitive = 7
TypeSignedLong TagTypePrimitive = 9
TypeSignedRational TagTypePrimitive = 10
// TypeAsciiNoNul is just a pseudo-type, for our own purposes.
TypeAsciiNoNul TagTypePrimitive = 0xf0
)
var (
typeLogger = log.NewLogger("exif.type")
)
var (
// TODO(dustin): Rename TypeNames() to typeNames() and add getter.
TypeNames = map[TagTypePrimitive]string{
TypeByte: "BYTE",
TypeAscii: "ASCII",
TypeShort: "SHORT",
TypeLong: "LONG",
TypeRational: "RATIONAL",
TypeUndefined: "UNDEFINED",
TypeSignedLong: "SLONG",
TypeSignedRational: "SRATIONAL",
TypeAsciiNoNul: "_ASCII_NO_NUL",
}
TypeNamesR = map[string]TagTypePrimitive{}
)
var (
// ErrNotEnoughData is used when there isn't enough data to accomodate what
// we're trying to parse (sizeof(type) * unit_count).
ErrNotEnoughData = errors.New("not enough data for type")
// ErrWrongType is used when we try to parse anything other than the
// current type.
ErrWrongType = errors.New("wrong type, can not parse")
// ErrUnhandledUnknownTag is used when we try to parse a tag that's
// recorded as an "unknown" type but not a documented tag (therefore
// leaving us not knowning how to read it).
ErrUnhandledUnknownTypedTag = errors.New("not a standard unknown-typed tag")
)
type Rational struct {
Numerator uint32
Denominator uint32
}
type SignedRational struct {
Numerator int32
Denominator int32
}
func TagTypeSize(tagType TagTypePrimitive) int {
// DEPRECATED(dustin): `(TagTypePrimitive).Size()` should be used, directly.
return tagType.Size()
}
// Format returns a stringified value for the given bytes. Automatically
// calculates count based on type size.
func Format(rawBytes []byte, tagType TagTypePrimitive, justFirst bool, byteOrder binary.ByteOrder) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): !! Add tests
typeSize := tagType.Size()
if len(rawBytes)%typeSize != 0 {
log.Panicf("byte-count (%d) does not align for [%s] type with a size of (%d) bytes", len(rawBytes), TypeNames[tagType], typeSize)
}
// unitCount is the calculated unit-count. This should equal the original
// value from the tag (pre-resolution).
unitCount := uint32(len(rawBytes) / typeSize)
// Truncate the items if it's not bytes or a string and we just want the first.
valueSuffix := ""
if justFirst == true && unitCount > 1 && tagType != TypeByte && tagType != TypeAscii && tagType != TypeAsciiNoNul {
unitCount = 1
valueSuffix = "..."
}
if tagType == TypeByte {
items, err := parser.ParseBytes(rawBytes, unitCount)
log.PanicIf(err)
return DumpBytesToString(items), nil
} else if tagType == TypeAscii {
phrase, err := parser.ParseAscii(rawBytes, unitCount)
log.PanicIf(err)
return phrase, nil
} else if tagType == TypeAsciiNoNul {
phrase, err := parser.ParseAsciiNoNul(rawBytes, unitCount)
log.PanicIf(err)
return phrase, nil
} else if tagType == TypeShort {
items, err := parser.ParseShorts(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
if len(items) > 0 {
if justFirst == true {
return fmt.Sprintf("%v%s", items[0], valueSuffix), nil
} else {
return fmt.Sprintf("%v", items), nil
}
} else {
return "", nil
}
} else if tagType == TypeLong {
items, err := parser.ParseLongs(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
if len(items) > 0 {
if justFirst == true {
return fmt.Sprintf("%v%s", items[0], valueSuffix), nil
} else {
return fmt.Sprintf("%v", items), nil
}
} else {
return "", nil
}
} else if tagType == TypeRational {
items, err := parser.ParseRationals(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
if len(items) > 0 {
parts := make([]string, len(items))
for i, r := range items {
parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator)
}
if justFirst == true {
return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil
} else {
return fmt.Sprintf("%v", parts), nil
}
} else {
return "", nil
}
} else if tagType == TypeSignedLong {
items, err := parser.ParseSignedLongs(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
if len(items) > 0 {
if justFirst == true {
return fmt.Sprintf("%v%s", items[0], valueSuffix), nil
} else {
return fmt.Sprintf("%v", items), nil
}
} else {
return "", nil
}
} else if tagType == TypeSignedRational {
items, err := parser.ParseSignedRationals(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
parts := make([]string, len(items))
for i, r := range items {
parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator)
}
if len(items) > 0 {
if justFirst == true {
return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil
} else {
return fmt.Sprintf("%v", parts), nil
}
} else {
return "", nil
}
} else {
// Affects only "unknown" values, in general.
log.Panicf("value of type [%s] can not be formatted into string", tagType.String())
// Never called.
return "", nil
}
}
func EncodeStringToBytes(tagType TagTypePrimitive, valueString string) (value interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if tagType == TypeUndefined {
// TODO(dustin): Circle back to this.
log.Panicf("undefined-type values are not supported")
}
if tagType == TypeByte {
return []byte(valueString), nil
} else if tagType == TypeAscii || tagType == TypeAsciiNoNul {
// Whether or not we're putting an NUL on the end is only relevant for
// byte-level encoding. This function really just supports a user
// interface.
return valueString, nil
} else if tagType == TypeShort {
n, err := strconv.ParseUint(valueString, 10, 16)
log.PanicIf(err)
return uint16(n), nil
} else if tagType == TypeLong {
n, err := strconv.ParseUint(valueString, 10, 32)
log.PanicIf(err)
return uint32(n), nil
} else if tagType == TypeRational {
parts := strings.SplitN(valueString, "/", 2)
numerator, err := strconv.ParseUint(parts[0], 10, 32)
log.PanicIf(err)
denominator, err := strconv.ParseUint(parts[1], 10, 32)
log.PanicIf(err)
return Rational{
Numerator: uint32(numerator),
Denominator: uint32(denominator),
}, nil
} else if tagType == TypeSignedLong {
n, err := strconv.ParseInt(valueString, 10, 32)
log.PanicIf(err)
return int32(n), nil
} else if tagType == TypeSignedRational {
parts := strings.SplitN(valueString, "/", 2)
numerator, err := strconv.ParseInt(parts[0], 10, 32)
log.PanicIf(err)
denominator, err := strconv.ParseInt(parts[1], 10, 32)
log.PanicIf(err)
return SignedRational{
Numerator: int32(numerator),
Denominator: int32(denominator),
}, nil
}
log.Panicf("from-string encoding for type not supported; this shouldn't happen: [%s]", tagType.String())
return nil, nil
}
func init() {
for typeId, typeName := range TypeNames {
TypeNamesR[typeName] = typeId
}
}

View file

@ -1,262 +0,0 @@
package exif
import (
"bytes"
"reflect"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
var (
typeEncodeLogger = log.NewLogger("exif.type_encode")
)
// EncodedData encapsulates the compound output of an encoding operation.
type EncodedData struct {
Type TagTypePrimitive
Encoded []byte
// TODO(dustin): Is this really necessary? We might have this just to correlate to the incoming stream format (raw bytes and a unit-count both for incoming and outgoing).
UnitCount uint32
}
type ValueEncoder struct {
byteOrder binary.ByteOrder
}
func NewValueEncoder(byteOrder binary.ByteOrder) *ValueEncoder {
return &ValueEncoder{
byteOrder: byteOrder,
}
}
func (ve *ValueEncoder) encodeBytes(value []uint8) (ed EncodedData, err error) {
ed.Type = TypeByte
ed.Encoded = []byte(value)
ed.UnitCount = uint32(len(value))
return ed, nil
}
func (ve *ValueEncoder) encodeAscii(value string) (ed EncodedData, err error) {
ed.Type = TypeAscii
ed.Encoded = []byte(value)
ed.Encoded = append(ed.Encoded, 0)
ed.UnitCount = uint32(len(ed.Encoded))
return ed, nil
}
// encodeAsciiNoNul returns a string encoded as a byte-string without a trailing
// NUL byte.
//
// Note that:
//
// 1. This type can not be automatically encoded using `Encode()`. The default
// mode is to encode *with* a trailing NUL byte using `encodeAscii`. Only
// certain undefined-type tags using an unterminated ASCII string and these
// are exceptional in nature.
//
// 2. The presence of this method allows us to completely test the complimentary
// no-nul parser.
//
func (ve *ValueEncoder) encodeAsciiNoNul(value string) (ed EncodedData, err error) {
ed.Type = TypeAsciiNoNul
ed.Encoded = []byte(value)
ed.UnitCount = uint32(len(ed.Encoded))
return ed, nil
}
func (ve *ValueEncoder) encodeShorts(value []uint16) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
ed.Encoded = make([]byte, ed.UnitCount*2)
for i := uint32(0); i < ed.UnitCount; i++ {
ve.byteOrder.PutUint16(ed.Encoded[i*2:(i+1)*2], value[i])
}
ed.Type = TypeShort
return ed, nil
}
func (ve *ValueEncoder) encodeLongs(value []uint32) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
ed.Encoded = make([]byte, ed.UnitCount*4)
for i := uint32(0); i < ed.UnitCount; i++ {
ve.byteOrder.PutUint32(ed.Encoded[i*4:(i+1)*4], value[i])
}
ed.Type = TypeLong
return ed, nil
}
func (ve *ValueEncoder) encodeRationals(value []Rational) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
ed.Encoded = make([]byte, ed.UnitCount*8)
for i := uint32(0); i < ed.UnitCount; i++ {
ve.byteOrder.PutUint32(ed.Encoded[i*8+0:i*8+4], value[i].Numerator)
ve.byteOrder.PutUint32(ed.Encoded[i*8+4:i*8+8], value[i].Denominator)
}
ed.Type = TypeRational
return ed, nil
}
func (ve *ValueEncoder) encodeSignedLongs(value []int32) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
b := bytes.NewBuffer(make([]byte, 0, 8*ed.UnitCount))
for i := uint32(0); i < ed.UnitCount; i++ {
err := binary.Write(b, ve.byteOrder, value[i])
log.PanicIf(err)
}
ed.Type = TypeSignedLong
ed.Encoded = b.Bytes()
return ed, nil
}
func (ve *ValueEncoder) encodeSignedRationals(value []SignedRational) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
b := bytes.NewBuffer(make([]byte, 0, 8*ed.UnitCount))
for i := uint32(0); i < ed.UnitCount; i++ {
err := binary.Write(b, ve.byteOrder, value[i].Numerator)
log.PanicIf(err)
err = binary.Write(b, ve.byteOrder, value[i].Denominator)
log.PanicIf(err)
}
ed.Type = TypeSignedRational
ed.Encoded = b.Bytes()
return ed, nil
}
// Encode returns bytes for the given value, infering type from the actual
// value. This does not support `TypeAsciiNoNull` (all strings are encoded as
// `TypeAscii`).
func (ve *ValueEncoder) Encode(value interface{}) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): This is redundant with EncodeWithType. Refactor one to use the other.
switch value.(type) {
case []byte:
ed, err = ve.encodeBytes(value.([]byte))
log.PanicIf(err)
case string:
ed, err = ve.encodeAscii(value.(string))
log.PanicIf(err)
case []uint16:
ed, err = ve.encodeShorts(value.([]uint16))
log.PanicIf(err)
case []uint32:
ed, err = ve.encodeLongs(value.([]uint32))
log.PanicIf(err)
case []Rational:
ed, err = ve.encodeRationals(value.([]Rational))
log.PanicIf(err)
case []int32:
ed, err = ve.encodeSignedLongs(value.([]int32))
log.PanicIf(err)
case []SignedRational:
ed, err = ve.encodeSignedRationals(value.([]SignedRational))
log.PanicIf(err)
default:
log.Panicf("value not encodable: [%s] [%v]", reflect.TypeOf(value), value)
}
return ed, nil
}
// EncodeWithType returns bytes for the given value, using the given `TagType`
// value to determine how to encode. This supports `TypeAsciiNoNul`.
func (ve *ValueEncoder) EncodeWithType(tt TagType, value interface{}) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): This is redundant with Encode. Refactor one to use the other.
switch tt.Type() {
case TypeByte:
ed, err = ve.encodeBytes(value.([]byte))
log.PanicIf(err)
case TypeAscii:
ed, err = ve.encodeAscii(value.(string))
log.PanicIf(err)
case TypeAsciiNoNul:
ed, err = ve.encodeAsciiNoNul(value.(string))
log.PanicIf(err)
case TypeShort:
ed, err = ve.encodeShorts(value.([]uint16))
log.PanicIf(err)
case TypeLong:
ed, err = ve.encodeLongs(value.([]uint32))
log.PanicIf(err)
case TypeRational:
ed, err = ve.encodeRationals(value.([]Rational))
log.PanicIf(err)
case TypeSignedLong:
ed, err = ve.encodeSignedLongs(value.([]int32))
log.PanicIf(err)
case TypeSignedRational:
ed, err = ve.encodeSignedRationals(value.([]SignedRational))
log.PanicIf(err)
default:
log.Panicf("value not encodable (with type): %v [%v]", tt, value)
}
return ed, nil
}

View file

@ -1,9 +0,0 @@
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.

View file

@ -1,34 +0,0 @@
package exif
import (
"github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common"
)
// TODO(dustin): This file now exists for backwards-compatibility only.
// NewIfdMapping returns a new IfdMapping struct.
func NewIfdMapping() (ifdMapping *exifcommon.IfdMapping) {
return exifcommon.NewIfdMapping()
}
// NewIfdMappingWithStandard retruns a new IfdMapping struct preloaded with the
// standard IFDs.
func NewIfdMappingWithStandard() (ifdMapping *exifcommon.IfdMapping) {
return exifcommon.NewIfdMappingWithStandard()
}
// LoadStandardIfds loads the standard IFDs into the mapping.
func LoadStandardIfds(im *exifcommon.IfdMapping) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
err = exifcommon.LoadStandardIfds(im)
log.PanicIf(err)
return nil
}

View file

@ -59,20 +59,19 @@ func NewIfdMapping() (ifdMapping *IfdMapping) {
// NewIfdMappingWithStandard retruns a new IfdMapping struct preloaded with the // NewIfdMappingWithStandard retruns a new IfdMapping struct preloaded with the
// standard IFDs. // standard IFDs.
func NewIfdMappingWithStandard() (ifdMapping *IfdMapping) { func NewIfdMappingWithStandard() (ifdMapping *IfdMapping, err error) {
defer func() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
err := log.Wrap(state.(error)) err = log.Wrap(state.(error))
log.Panic(err)
} }
}() }()
im := NewIfdMapping() im := NewIfdMapping()
err := LoadStandardIfds(im) err = LoadStandardIfds(im)
log.PanicIf(err) log.PanicIf(err)
return im return im, nil
} }
// Get returns the node given the path slice. // Get returns the node given the path slice.
@ -650,10 +649,3 @@ var (
// Ifd1StandardIfdIdentity represents the IFD path for IFD1. // Ifd1StandardIfdIdentity represents the IFD path for IFD1.
Ifd1StandardIfdIdentity = NewIfdIdentity(rootStandardIfd, IfdIdentityPart{"IFD", 1}) Ifd1StandardIfdIdentity = NewIfdIdentity(rootStandardIfd, IfdIdentityPart{"IFD", 1})
) )
var (
IfdPathStandard = IfdStandardIfdIdentity
IfdPathStandardExif = IfdExifStandardIfdIdentity
IfdPathStandardExifIop = IfdExifIopStandardIfdIdentity
IfdPathStandardGps = IfdGpsInfoStandardIfdIdentity
)

View file

@ -2,6 +2,8 @@ package exifcommon
import ( import (
"bytes" "bytes"
"errors"
"math"
"encoding/binary" "encoding/binary"
@ -12,6 +14,10 @@ var (
parserLogger = log.NewLogger("exifcommon.parser") parserLogger = log.NewLogger("exifcommon.parser")
) )
var (
ErrParseFail = errors.New("parse failure")
)
// Parser knows how to parse all well-defined, encoded EXIF types. // Parser knows how to parse all well-defined, encoded EXIF types.
type Parser struct { type Parser struct {
} }
@ -56,7 +62,18 @@ func (p *Parser) ParseAscii(data []byte, unitCount uint32) (value string, err er
if len(data) == 0 || data[count-1] != 0 { if len(data) == 0 || data[count-1] != 0 {
s := string(data[:count]) s := string(data[:count])
parserLogger.Warningf(nil, "ascii not terminated with nul as expected: [%v]", s) 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 return s, nil
} }
@ -135,6 +152,50 @@ func (p *Parser) ParseLongs(data []byte, unitCount uint32, byteOrder binary.Byte
return value, nil 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. // 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) { func (p *Parser) ParseRationals(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []Rational, err error) {
defer func() { defer func() {

View file

@ -6,6 +6,7 @@ import (
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"unicode"
"encoding/binary" "encoding/binary"
@ -63,6 +64,12 @@ const (
// TypeSignedRational describes an encoded list of signed rationals. // TypeSignedRational describes an encoded list of signed rationals.
TypeSignedRational TagTypePrimitive = 10 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 is just a pseudo-type, for our own purposes.
TypeAsciiNoNul TagTypePrimitive = 0xf0 TypeAsciiNoNul TagTypePrimitive = 0xf0
) )
@ -74,23 +81,19 @@ func (typeType TagTypePrimitive) String() string {
// Size returns the size of one atomic unit of the type. // Size returns the size of one atomic unit of the type.
func (tagType TagTypePrimitive) Size() int { func (tagType TagTypePrimitive) Size() int {
if tagType == TypeByte { switch tagType {
case TypeByte, TypeAscii, TypeAsciiNoNul:
return 1 return 1
} else if tagType == TypeAscii || tagType == TypeAsciiNoNul { case TypeShort:
return 1
} else if tagType == TypeShort {
return 2 return 2
} else if tagType == TypeLong { case TypeLong, TypeSignedLong, TypeFloat:
return 4 return 4
} else if tagType == TypeRational { case TypeRational, TypeSignedRational, TypeDouble:
return 8 return 8
} else if tagType == TypeSignedLong { default:
return 4 log.Panicf("can not determine tag-value size for type (%d): [%s]",
} else if tagType == TypeSignedRational { tagType,
return 8 TypeNames[tagType])
} else {
log.Panicf("can not determine tag-value size for type (%d): [%s]", tagType, TypeNames[tagType])
// Never called. // Never called.
return 0 return 0
} }
@ -109,6 +112,8 @@ func (tagType TagTypePrimitive) IsValid() bool {
tagType == TypeRational || tagType == TypeRational ||
tagType == TypeSignedLong || tagType == TypeSignedLong ||
tagType == TypeSignedRational || tagType == TypeSignedRational ||
tagType == TypeFloat ||
tagType == TypeDouble ||
tagType == TypeUndefined tagType == TypeUndefined
} }
@ -123,6 +128,8 @@ var (
TypeUndefined: "UNDEFINED", TypeUndefined: "UNDEFINED",
TypeSignedLong: "SLONG", TypeSignedLong: "SLONG",
TypeSignedRational: "SRATIONAL", TypeSignedRational: "SRATIONAL",
TypeFloat: "FLOAT",
TypeDouble: "DOUBLE",
TypeAsciiNoNul: "_ASCII_NO_NUL", TypeAsciiNoNul: "_ASCII_NO_NUL",
} }
@ -148,6 +155,19 @@ type SignedRational struct {
Denominator int32 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 // Format returns a stringified value for the given encoding. Automatically
// parses. Automatically calculates count based on type size. This function // parses. Automatically calculates count based on type size. This function
// also supports undefined-type values (the ones that we support, anyway) by // also supports undefined-type values (the ones that we support, anyway) by
@ -166,37 +186,36 @@ func FormatFromType(value interface{}, justFirst bool) (phrase string, err error
case []byte: case []byte:
return DumpBytesToString(t), nil return DumpBytesToString(t), nil
case string: 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 return t, nil
case []uint16: case []uint16, []uint32, []int32, []float64, []float32:
if len(t) == 0 { val := reflect.ValueOf(t)
if val.Len() == 0 {
return "", nil return "", nil
} }
if justFirst == true { if justFirst == true {
var valueSuffix string var valueSuffix string
if len(t) > 1 { if val.Len() > 1 {
valueSuffix = "..." valueSuffix = "..."
} }
return fmt.Sprintf("%v%s", t[0], valueSuffix), nil return fmt.Sprintf("%v%s", val.Index(0), valueSuffix), nil
} }
return fmt.Sprintf("%v", t), nil return fmt.Sprintf("%v", val), nil
case []uint32:
if len(t) == 0 {
return "", nil
}
if justFirst == true {
var valueSuffix string
if len(t) > 1 {
valueSuffix = "..."
}
return fmt.Sprintf("%v%s", t[0], valueSuffix), nil
}
return fmt.Sprintf("%v", t), nil
case []Rational: case []Rational:
if len(t) == 0 { if len(t) == 0 {
return "", nil return "", nil
@ -221,21 +240,6 @@ func FormatFromType(value interface{}, justFirst bool) (phrase string, err error
} }
return fmt.Sprintf("%v", parts), nil return fmt.Sprintf("%v", parts), nil
case []int32:
if len(t) == 0 {
return "", nil
}
if justFirst == true {
var valueSuffix string
if len(t) > 1 {
valueSuffix = "..."
}
return fmt.Sprintf("%v%s", t[0], valueSuffix), nil
}
return fmt.Sprintf("%v", t), nil
case []SignedRational: case []SignedRational:
if len(t) == 0 { if len(t) == 0 {
return "", nil return "", nil
@ -261,8 +265,14 @@ func FormatFromType(value interface{}, justFirst bool) (phrase string, err error
return fmt.Sprintf("%v", parts), nil return fmt.Sprintf("%v", parts), nil
case fmt.Stringer: 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). // An undefined value that is documented (or that we otherwise support).
return t.String(), nil return s, nil
default: default:
// Affects only "unknown" values, in general. // Affects only "unknown" values, in general.
log.Panicf("type can not be formatted into string: %v", reflect.TypeOf(value).Name()) log.Panicf("type can not be formatted into string: %v", reflect.TypeOf(value).Name())
@ -323,6 +333,16 @@ func FormatFromBytes(rawBytes []byte, tagType TagTypePrimitive, justFirst bool,
value, err = parser.ParseLongs(rawBytes, unitCount, byteOrder) value, err = parser.ParseLongs(rawBytes, unitCount, byteOrder)
log.PanicIf(err) 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: case TypeRational:
var err error var err error
@ -407,6 +427,16 @@ func TranslateStringToType(tagType TagTypePrimitive, valueString string) (value
log.PanicIf(err) log.PanicIf(err)
return int32(n), nil 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 { } else if tagType == TypeSignedRational {
parts := strings.SplitN(valueString, "/", 2) parts := strings.SplitN(valueString, "/", 2)

View file

@ -1,8 +1,9 @@
package exif package exifcommon
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"reflect"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -10,6 +11,11 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
) )
var (
timeType = reflect.TypeOf(time.Time{})
)
// DumpBytes prints a list of hex-encoded bytes.
func DumpBytes(data []byte) { func DumpBytes(data []byte) {
fmt.Printf("DUMP: ") fmt.Printf("DUMP: ")
for _, x := range data { for _, x := range data {
@ -19,6 +25,8 @@ func DumpBytes(data []byte) {
fmt.Printf("\n") fmt.Printf("\n")
} }
// DumpBytesClause prints a list like DumpBytes(), but encapsulated in
// "[]byte { ... }".
func DumpBytesClause(data []byte) { func DumpBytesClause(data []byte) {
fmt.Printf("DUMP: ") fmt.Printf("DUMP: ")
@ -35,6 +43,7 @@ func DumpBytesClause(data []byte) {
fmt.Printf(" }\n") fmt.Printf(" }\n")
} }
// DumpBytesToString returns a stringified list of hex-encoded bytes.
func DumpBytesToString(data []byte) string { func DumpBytesToString(data []byte) string {
b := new(bytes.Buffer) b := new(bytes.Buffer)
@ -51,6 +60,7 @@ func DumpBytesToString(data []byte) string {
return b.String() return b.String()
} }
// DumpBytesClauseToString returns a comma-separated list of hex-encoded bytes.
func DumpBytesClauseToString(data []byte) string { func DumpBytesClauseToString(data []byte) string {
b := new(bytes.Buffer) b := new(bytes.Buffer)
@ -67,6 +77,14 @@ func DumpBytesClauseToString(data []byte) string {
return b.String() 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 // ParseExifFullTimestamp parses dates like "2018:11:30 13:01:49" into a UTC
// `time.Time` struct. // `time.Time` struct.
func ParseExifFullTimestamp(fullTimestampPhrase string) (timestamp time.Time, err error) { func ParseExifFullTimestamp(fullTimestampPhrase string) (timestamp time.Time, err error) {
@ -79,6 +97,10 @@ func ParseExifFullTimestamp(fullTimestampPhrase string) (timestamp time.Time, er
parts := strings.Split(fullTimestampPhrase, " ") parts := strings.Split(fullTimestampPhrase, " ")
datestampValue, timestampValue := parts[0], parts[1] datestampValue, timestampValue := parts[0], parts[1]
// Normalize the separators.
datestampValue = strings.ReplaceAll(datestampValue, "-", ":")
timestampValue = strings.ReplaceAll(timestampValue, "-", ":")
dateParts := strings.Split(datestampValue, ":") dateParts := strings.Split(datestampValue, ":")
year, err := strconv.ParseUint(dateParts[0], 10, 16) year, err := strconv.ParseUint(dateParts[0], 10, 16)
@ -117,106 +139,10 @@ func ParseExifFullTimestamp(fullTimestampPhrase string) (timestamp time.Time, er
return timestamp, nil return timestamp, nil
} }
// ExifFullTimestampString produces a string like "2018:11:30 13:01:49" from a // IsTime returns true if the value is a `time.Time`.
// `time.Time` struct. It will attempt to convert to UTC first. func IsTime(v interface{}) bool {
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()) // TODO(dustin): Add test
}
return reflect.TypeOf(v) == timeType
// ExifTag is one simple representation of a tag in a flat list of all of them.
type ExifTag struct {
IfdPath string `json:"ifd_path"`
TagId uint16 `json:"id"`
TagName string `json:"name"`
TagTypeId TagTypePrimitive `json:"type_id"`
TagTypeName string `json:"type_name"`
Value interface{} `json:"value"`
ValueBytes []byte `json:"value_bytes"`
ChildIfdPath string `json:"child_ifd_path"`
}
// String returns a string representation.
func (et ExifTag) String() string {
return fmt.Sprintf("ExifTag<IFD-PATH=[%s] TAG-ID=(0x%02x) TAG-NAME=[%s] TAG-TYPE=[%s] VALUE=[%v] VALUE-BYTES=(%d) CHILD-IFD-PATH=[%s]", et.IfdPath, et.TagId, et.TagName, et.TagTypeName, et.Value, len(et.ValueBytes), et.ChildIfdPath)
}
// GetFlatExifData returns a simple, flat representation of all tags.
func GetFlatExifData(exifData []byte) (exifTags []ExifTag, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
im := NewIfdMappingWithStandard()
ti := NewTagIndex()
_, index, err := Collect(im, ti, exifData)
log.PanicIf(err)
q := []*Ifd{index.RootIfd}
exifTags = make([]ExifTag, 0)
for len(q) > 0 {
var ifd *Ifd
ifd, q = q[0], q[1:]
ti := NewTagIndex()
for _, ite := range ifd.Entries {
tagName := ""
it, err := ti.Get(ifd.IfdPath, ite.TagId)
if err != nil {
// If it's a non-standard tag, just leave the name blank.
if log.Is(err, ErrTagNotFound) != true {
log.PanicIf(err)
}
} else {
tagName = it.Name
}
value, err := ifd.TagValue(ite)
if err != nil {
if err == ErrUnhandledUnknownTypedTag {
value = UnparseableUnknownTagValuePlaceholder
} else {
log.Panic(err)
}
}
valueBytes, err := ifd.TagValueBytes(ite)
if err != nil && err != ErrUnhandledUnknownTypedTag {
log.Panic(err)
}
et := ExifTag{
IfdPath: ifd.IfdPath,
TagId: ite.TagId,
TagName: tagName,
TagTypeId: ite.TagType,
TagTypeName: TypeNames[ite.TagType],
Value: value,
ValueBytes: valueBytes,
ChildIfdPath: ite.ChildIfdPath,
}
exifTags = append(exifTags, et)
}
for _, childIfd := range ifd.Children {
q = append(q, childIfd)
}
if ifd.NextIfd != nil {
q = append(q, ifd.NextIfd)
}
}
return exifTags, nil
} }

View file

@ -2,6 +2,7 @@ package exifcommon
import ( import (
"errors" "errors"
"io"
"encoding/binary" "encoding/binary"
@ -21,10 +22,10 @@ var (
// ValueContext embeds all of the parameters required to find and extract the // ValueContext embeds all of the parameters required to find and extract the
// actual tag value. // actual tag value.
type ValueContext struct { type ValueContext struct {
unitCount uint32 unitCount uint32
valueOffset uint32 valueOffset uint32
rawValueOffset []byte rawValueOffset []byte
addressableData []byte rs io.ReadSeeker
tagType TagTypePrimitive tagType TagTypePrimitive
byteOrder binary.ByteOrder byteOrder binary.ByteOrder
@ -40,12 +41,12 @@ type ValueContext struct {
// TODO(dustin): We can update newValueContext() to derive `valueOffset` itself (from `rawValueOffset`). // TODO(dustin): We can update newValueContext() to derive `valueOffset` itself (from `rawValueOffset`).
// NewValueContext returns a new ValueContext struct. // NewValueContext returns a new ValueContext struct.
func NewValueContext(ifdPath string, tagId uint16, unitCount, valueOffset uint32, rawValueOffset, addressableData []byte, tagType TagTypePrimitive, byteOrder binary.ByteOrder) *ValueContext { func NewValueContext(ifdPath string, tagId uint16, unitCount, valueOffset uint32, rawValueOffset []byte, rs io.ReadSeeker, tagType TagTypePrimitive, byteOrder binary.ByteOrder) *ValueContext {
return &ValueContext{ return &ValueContext{
unitCount: unitCount, unitCount: unitCount,
valueOffset: valueOffset, valueOffset: valueOffset,
rawValueOffset: rawValueOffset, rawValueOffset: rawValueOffset,
addressableData: addressableData, rs: rs,
tagType: tagType, tagType: tagType,
byteOrder: byteOrder, byteOrder: byteOrder,
@ -82,8 +83,11 @@ func (vc *ValueContext) RawValueOffset() []byte {
} }
// AddressableData returns the block of data that we can dereference into. // AddressableData returns the block of data that we can dereference into.
func (vc *ValueContext) AddressableData() []byte { func (vc *ValueContext) AddressableData() io.ReadSeeker {
return vc.addressableData
// RELEASE)dustin): Rename from AddressableData() to ReadSeeker()
return vc.rs
} }
// ByteOrder returns the byte-order of numbers. // ByteOrder returns the byte-order of numbers.
@ -152,7 +156,15 @@ func (vc *ValueContext) readRawEncoded() (rawBytes []byte, err error) {
return vc.rawValueOffset[:byteLength], nil return vc.rawValueOffset[:byteLength], nil
} }
return vc.addressableData[vc.valueOffset : vc.valueOffset+vc.unitCount*unitSizeRaw], 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 // GetFarOffset returns the offset if the value is not embedded [within the
@ -303,6 +315,40 @@ func (vc *ValueContext) ReadLongs() (value []uint32, err error) {
return value, nil 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- // ReadRationals parses the list of encoded, unsigned rationals from the value-
// context. // context.
func (vc *ValueContext) ReadRationals() (value []Rational, err error) { func (vc *ValueContext) ReadRationals() (value []Rational, err error) {
@ -393,6 +439,12 @@ func (vc *ValueContext) Values() (values interface{}, err error) {
} else if vc.tagType == TypeSignedRational { } else if vc.tagType == TypeSignedRational {
values, err = vc.ReadSignedRationals() values, err = vc.ReadSignedRationals()
log.PanicIf(err) 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 { } else if vc.tagType == TypeUndefined {
log.Panicf("will not parse undefined-type value") log.Panicf("will not parse undefined-type value")

View file

@ -2,6 +2,7 @@ package exifcommon
import ( import (
"bytes" "bytes"
"math"
"reflect" "reflect"
"time" "time"
@ -113,6 +114,44 @@ func (ve *ValueEncoder) encodeLongs(value []uint32) (ed EncodedData, err error)
return ed, nil 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) { func (ve *ValueEncoder) encodeRationals(value []Rational) (ed EncodedData, err error) {
defer func() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
@ -190,33 +229,38 @@ func (ve *ValueEncoder) Encode(value interface{}) (ed EncodedData, err error) {
} }
}() }()
switch value.(type) { switch t := value.(type) {
case []byte: case []byte:
ed, err = ve.encodeBytes(value.([]byte)) ed, err = ve.encodeBytes(t)
log.PanicIf(err) log.PanicIf(err)
case string: case string:
ed, err = ve.encodeAscii(value.(string)) ed, err = ve.encodeAscii(t)
log.PanicIf(err) log.PanicIf(err)
case []uint16: case []uint16:
ed, err = ve.encodeShorts(value.([]uint16)) ed, err = ve.encodeShorts(t)
log.PanicIf(err) log.PanicIf(err)
case []uint32: case []uint32:
ed, err = ve.encodeLongs(value.([]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) log.PanicIf(err)
case []Rational: case []Rational:
ed, err = ve.encodeRationals(value.([]Rational)) ed, err = ve.encodeRationals(t)
log.PanicIf(err) log.PanicIf(err)
case []int32: case []int32:
ed, err = ve.encodeSignedLongs(value.([]int32)) ed, err = ve.encodeSignedLongs(t)
log.PanicIf(err) log.PanicIf(err)
case []SignedRational: case []SignedRational:
ed, err = ve.encodeSignedRationals(value.([]SignedRational)) ed, err = ve.encodeSignedRationals(t)
log.PanicIf(err) log.PanicIf(err)
case time.Time: case time.Time:
// For convenience, if the user doesn't want to deal with translation // For convenience, if the user doesn't want to deal with translation
// semantics with timestamps. // semantics with timestamps.
t := value.(time.Time)
s := ExifFullTimestampString(t) s := ExifFullTimestampString(t)
ed, err = ve.encodeAscii(s) ed, err = ve.encodeAscii(s)

50
vendor/github.com/dsoprea/go-exif/v3/data_layer.go generated vendored Normal file
View file

@ -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
}

View file

@ -13,7 +13,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
const ( const (
@ -70,10 +70,58 @@ func SearchAndExtractExif(data []byte) (rawExif []byte, err error) {
return rawExif, nil return rawExif, nil
} }
// SearchAndExtractExifWithReader searches for an EXIF blob using an // SearchAndExtractExifN searches for an EXIF blob in the byte-slice, but skips
// `io.Reader`. We can't know how much long the EXIF data is without parsing it, // the given number of EXIF blocks first. This is a forensics tool that helps
// so this will likely grab up a lot of the image-data, too. // identify multiple EXIF blocks in a file.
func SearchAndExtractExifWithReader(r io.Reader) (rawExif []byte, err error) { 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() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
err = log.Wrap(state.(error)) err = log.Wrap(state.(error))
@ -85,13 +133,12 @@ func SearchAndExtractExifWithReader(r io.Reader) (rawExif []byte, err error) {
// least, again, with JPEGs). // least, again, with JPEGs).
br := bufio.NewReader(r) br := bufio.NewReader(r)
discarded := 0
for { for {
window, err := br.Peek(ExifSignatureLength) window, err := br.Peek(ExifSignatureLength)
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
return nil, ErrNoExif return nil, 0, ErrNoExif
} }
log.Panic(err) log.Panic(err)
@ -122,6 +169,30 @@ func SearchAndExtractExifWithReader(r io.Reader) (rawExif []byte, err error) {
rawExif, err = ioutil.ReadAll(br) rawExif, err = ioutil.ReadAll(br)
log.PanicIf(err) 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 return rawExif, nil
} }
@ -179,9 +250,11 @@ func ParseExifHeader(data []byte) (eh ExifHeader, err error) {
} }
if bytes.Equal(data[:4], ExifBigEndianSignature[:]) == true { if bytes.Equal(data[:4], ExifBigEndianSignature[:]) == true {
exifLogger.Debugf(nil, "Byte-order is big-endian.")
eh.ByteOrder = binary.BigEndian eh.ByteOrder = binary.BigEndian
} else if bytes.Equal(data[:4], ExifLittleEndianSignature[:]) == true { } else if bytes.Equal(data[:4], ExifLittleEndianSignature[:]) == true {
eh.ByteOrder = binary.LittleEndian eh.ByteOrder = binary.LittleEndian
exifLogger.Debugf(nil, "Byte-order is little-endian.")
} else { } else {
return eh, ErrNoExif return eh, ErrNoExif
} }
@ -192,7 +265,7 @@ func ParseExifHeader(data []byte) (eh ExifHeader, err error) {
} }
// Visit recursively invokes a callback for every tag. // Visit recursively invokes a callback for every tag.
func Visit(rootIfdIdentity *exifcommon.IfdIdentity, ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, exifData []byte, visitor TagVisitorFn) (eh ExifHeader, furthestOffset uint32, err error) { func Visit(rootIfdIdentity *exifcommon.IfdIdentity, ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, exifData []byte, visitor TagVisitorFn, so *ScanOptions) (eh ExifHeader, furthestOffset uint32, err error) {
defer func() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
err = log.Wrap(state.(error)) err = log.Wrap(state.(error))
@ -202,9 +275,10 @@ func Visit(rootIfdIdentity *exifcommon.IfdIdentity, ifdMapping *exifcommon.IfdMa
eh, err = ParseExifHeader(exifData) eh, err = ParseExifHeader(exifData)
log.PanicIf(err) log.PanicIf(err)
ie := NewIfdEnumerate(ifdMapping, tagIndex, exifData, eh.ByteOrder) ebs := NewExifReadSeekerWithBytes(exifData)
ie := NewIfdEnumerate(ifdMapping, tagIndex, ebs, eh.ByteOrder)
_, err = ie.Scan(rootIfdIdentity, eh.FirstIfdOffset, visitor) _, err = ie.Scan(rootIfdIdentity, eh.FirstIfdOffset, visitor, so)
log.PanicIf(err) log.PanicIf(err)
furthestOffset = ie.FurthestOffset() furthestOffset = ie.FurthestOffset()
@ -223,7 +297,8 @@ func Collect(ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, exifData []b
eh, err = ParseExifHeader(exifData) eh, err = ParseExifHeader(exifData)
log.PanicIf(err) log.PanicIf(err)
ie := NewIfdEnumerate(ifdMapping, tagIndex, exifData, eh.ByteOrder) ebs := NewExifReadSeekerWithBytes(exifData)
ie := NewIfdEnumerate(ifdMapping, tagIndex, ebs, eh.ByteOrder)
index, err = ie.Collect(eh.FirstIfdOffset) index, err = ie.Collect(eh.FirstIfdOffset)
log.PanicIf(err) log.PanicIf(err)

View file

@ -8,7 +8,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/golang/geo/s2" "github.com/golang/geo/s2"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
var ( var (

View file

@ -14,8 +14,8 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
"github.com/dsoprea/go-exif/v2/undefined" "github.com/dsoprea/go-exif/v3/undefined"
) )
var ( var (
@ -262,8 +262,8 @@ func NewIfdBuilderWithExistingIfd(ifd *Ifd) (ib *IfdBuilder) {
ib = &IfdBuilder{ ib = &IfdBuilder{
ifdIdentity: ifd.IfdIdentity(), ifdIdentity: ifd.IfdIdentity(),
byteOrder: ifd.ByteOrder, byteOrder: ifd.ByteOrder(),
existingOffset: ifd.Offset, existingOffset: ifd.Offset(),
ifdMapping: ifd.ifdMapping, ifdMapping: ifd.ifdMapping,
tagIndex: ifd.tagIndex, tagIndex: ifd.tagIndex,
} }
@ -276,12 +276,12 @@ func NewIfdBuilderWithExistingIfd(ifd *Ifd) (ib *IfdBuilder) {
func NewIfdBuilderFromExistingChain(rootIfd *Ifd) (firstIb *IfdBuilder) { func NewIfdBuilderFromExistingChain(rootIfd *Ifd) (firstIb *IfdBuilder) {
var lastIb *IfdBuilder var lastIb *IfdBuilder
i := 0 i := 0
for thisExistingIfd := rootIfd; thisExistingIfd != nil; thisExistingIfd = thisExistingIfd.NextIfd { for thisExistingIfd := rootIfd; thisExistingIfd != nil; thisExistingIfd = thisExistingIfd.nextIfd {
newIb := NewIfdBuilder( newIb := NewIfdBuilder(
rootIfd.ifdMapping, rootIfd.ifdMapping,
rootIfd.tagIndex, rootIfd.tagIndex,
rootIfd.ifdIdentity, rootIfd.ifdIdentity,
thisExistingIfd.ByteOrder) thisExistingIfd.ByteOrder())
if firstIb == nil { if firstIb == nil {
firstIb = newIb firstIb = newIb
@ -1005,7 +1005,7 @@ func (ib *IfdBuilder) AddTagsFromExisting(ifd *Ifd, includeTagIds []uint16, excl
log.Panic(err) log.Panic(err)
} }
for i, ite := range ifd.Entries { for i, ite := range ifd.Entries() {
if ite.IsThumbnailOffset() == true || ite.IsThumbnailSize() { if ite.IsThumbnailOffset() == true || ite.IsThumbnailSize() {
// These will be added on-the-fly when we encode. // These will be added on-the-fly when we encode.
continue continue
@ -1051,11 +1051,11 @@ func (ib *IfdBuilder) AddTagsFromExisting(ifd *Ifd, includeTagIds []uint16, excl
// this IFD represents this specific child IFD. // this IFD represents this specific child IFD.
var childIfd *Ifd var childIfd *Ifd
for _, thisChildIfd := range ifd.Children { for _, thisChildIfd := range ifd.Children() {
if thisChildIfd.ParentTagIndex != i { if thisChildIfd.ParentTagIndex() != i {
continue continue
} else if thisChildIfd.ifdIdentity.TagId() != 0xffff && thisChildIfd.ifdIdentity.TagId() != ite.TagId() { } 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) log.Panicf("child-IFD tag is not correct: TAG-POSITION=(%d) ITE=%s CHILD-IFD=%s", thisChildIfd.ParentTagIndex(), ite, thisChildIfd)
} }
childIfd = thisChildIfd childIfd = thisChildIfd
@ -1063,9 +1063,9 @@ func (ib *IfdBuilder) AddTagsFromExisting(ifd *Ifd, includeTagIds []uint16, excl
} }
if childIfd == nil { if childIfd == nil {
childTagIds := make([]string, len(ifd.Children)) childTagIds := make([]string, len(ifd.Children()))
for j, childIfd := range ifd.Children { for j, childIfd := range ifd.Children() {
childTagIds[j] = fmt.Sprintf("0x%04x (parent tag-position %d)", childIfd.ifdIdentity.TagId(), childIfd.ParentTagIndex) 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) 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)

View file

@ -9,7 +9,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
const ( const (

View file

@ -13,8 +13,8 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
"github.com/dsoprea/go-exif/v2/undefined" "github.com/dsoprea/go-exif/v3/undefined"
) )
var ( var (
@ -75,23 +75,23 @@ var (
// statically-sized records. So, the tags (though notnecessarily their values) // statically-sized records. So, the tags (though notnecessarily their values)
// are fairly simple to enumerate. // are fairly simple to enumerate.
type byteParser struct { type byteParser struct {
byteOrder binary.ByteOrder byteOrder binary.ByteOrder
addressableData []byte rs io.ReadSeeker
ifdOffset uint32 ifdOffset uint32
currentOffset uint32 currentOffset uint32
} }
func newByteParser(addressableData []byte, byteOrder binary.ByteOrder, ifdOffset uint32) (bp *byteParser, err error) { // newByteParser returns a new byteParser struct.
if ifdOffset >= uint32(len(addressableData)) { //
return nil, ErrOffsetInvalid // 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 // TODO(dustin): Add test
bp = &byteParser{ bp = &byteParser{
addressableData: addressableData, rs: rs,
byteOrder: byteOrder, byteOrder: byteOrder,
currentOffset: ifdOffset, currentOffset: initialOffset,
} }
return bp, nil return bp, nil
@ -109,13 +109,13 @@ func (bp *byteParser) getUint16() (value uint16, raw []byte, err error) {
// TODO(dustin): Add test // TODO(dustin): Add test
needBytes := uint32(2) needBytes := 2
if bp.currentOffset+needBytes > uint32(len(bp.addressableData)) { raw = make([]byte, needBytes)
return 0, nil, io.EOF
} _, err = io.ReadFull(bp.rs, raw)
log.PanicIf(err)
raw = bp.addressableData[bp.currentOffset : bp.currentOffset+needBytes]
value = bp.byteOrder.Uint16(raw) value = bp.byteOrder.Uint16(raw)
bp.currentOffset += uint32(needBytes) bp.currentOffset += uint32(needBytes)
@ -135,13 +135,13 @@ func (bp *byteParser) getUint32() (value uint32, raw []byte, err error) {
// TODO(dustin): Add test // TODO(dustin): Add test
needBytes := uint32(4) needBytes := 4
if bp.currentOffset+needBytes > uint32(len(bp.addressableData)) { raw = make([]byte, needBytes)
return 0, nil, io.EOF
} _, err = io.ReadFull(bp.rs, raw)
log.PanicIf(err)
raw = bp.addressableData[bp.currentOffset : bp.currentOffset+needBytes]
value = bp.byteOrder.Uint32(raw) value = bp.byteOrder.Uint32(raw)
bp.currentOffset += uint32(needBytes) bp.currentOffset += uint32(needBytes)
@ -158,20 +158,24 @@ func (bp *byteParser) CurrentOffset() uint32 {
// IfdEnumerate is the main enumeration type. It knows how to parse the IFD // IfdEnumerate is the main enumeration type. It knows how to parse the IFD
// containers in the EXIF blob. // containers in the EXIF blob.
type IfdEnumerate struct { type IfdEnumerate struct {
exifData []byte ebs ExifBlobSeeker
byteOrder binary.ByteOrder byteOrder binary.ByteOrder
tagIndex *TagIndex tagIndex *TagIndex
ifdMapping *exifcommon.IfdMapping ifdMapping *exifcommon.IfdMapping
furthestOffset uint32 furthestOffset uint32
visitedIfdOffsets map[uint32]struct{}
} }
// NewIfdEnumerate returns a new instance of IfdEnumerate. // NewIfdEnumerate returns a new instance of IfdEnumerate.
func NewIfdEnumerate(ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, exifData []byte, byteOrder binary.ByteOrder) *IfdEnumerate { func NewIfdEnumerate(ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, ebs ExifBlobSeeker, byteOrder binary.ByteOrder) *IfdEnumerate {
return &IfdEnumerate{ return &IfdEnumerate{
exifData: exifData, ebs: ebs,
byteOrder: byteOrder, byteOrder: byteOrder,
ifdMapping: ifdMapping, ifdMapping: ifdMapping,
tagIndex: tagIndex, tagIndex: tagIndex,
visitedIfdOffsets: make(map[uint32]struct{}),
} }
} }
@ -182,11 +186,16 @@ func (ie *IfdEnumerate) getByteParser(ifdOffset uint32) (bp *byteParser, err err
} }
}() }()
initialOffset := ExifAddressableAreaStart + ifdOffset
rs, err := ie.ebs.GetReadSeeker(int64(initialOffset))
log.PanicIf(err)
bp, err = bp, err =
newByteParser( newByteParser(
ie.exifData[ExifAddressableAreaStart:], rs,
ie.byteOrder, ie.byteOrder,
ifdOffset) initialOffset)
if err != nil { if err != nil {
if err == ErrOffsetInvalid { if err == ErrOffsetInvalid {
@ -220,15 +229,63 @@ func (ie *IfdEnumerate) parseTag(ii *exifcommon.IfdIdentity, tagPosition int, bp
valueOffset, rawValueOffset, err := bp.getUint32() valueOffset, rawValueOffset, err := bp.getUint32()
log.PanicIf(err) log.PanicIf(err)
// Check whether the embedded type indicator is valid.
if tagType.IsValid() == false { 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{ ite = &IfdTagEntry{
tagId: tagId, tagId: tagId,
tagType: tagType, tagType: tagType,
} }
log.Panic(ErrTagTypeNotValid) 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( ite = newIfdTagEntry(
ii, ii,
tagId, tagId,
@ -237,7 +294,7 @@ func (ie *IfdEnumerate) parseTag(ii *exifcommon.IfdIdentity, tagPosition int, bp
unitCount, unitCount,
valueOffset, valueOffset,
rawValueOffset, rawValueOffset,
ie.exifData[ExifAddressableAreaStart:], rs,
ie.byteOrder) ie.byteOrder)
ifdPath := ii.UnindexedString() ifdPath := ii.UnindexedString()
@ -263,10 +320,10 @@ func (ie *IfdEnumerate) parseTag(ii *exifcommon.IfdIdentity, tagPosition int, bp
} }
// TagVisitorFn is called for each tag when enumerating through the EXIF. // TagVisitorFn is called for each tag when enumerating through the EXIF.
type TagVisitorFn func(fqIfdPath string, ifdIndex int, ite *IfdTagEntry) (err error) type TagVisitorFn func(ite *IfdTagEntry) (err error)
// postparseTag do some tag-level processing here following the parse of each. // tagPostParse do some tag-level processing here following the parse of each.
func (ie *IfdEnumerate) postparseTag(ite *IfdTagEntry, med *MiscellaneousExifData) (err error) { func (ie *IfdEnumerate) tagPostParse(ite *IfdTagEntry, med *MiscellaneousExifData) (err error) {
defer func() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
err = log.Wrap(state.(error)) err = log.Wrap(state.(error))
@ -355,7 +412,7 @@ func (ie *IfdEnumerate) postparseTag(ite *IfdTagEntry, med *MiscellaneousExifDat
// tag should ever be repeated, and b) all but one had an incorrect // 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 // type and caused parsing/conversion woes. So, this is a quick fix
// for those scenarios. // for those scenarios.
if it.DoesSupportType(tagType) == false { if ie.tagIndex.UniversalSearch() == false && it.DoesSupportType(tagType) == false {
ifdEnumerateLogger.Warningf(nil, ifdEnumerateLogger.Warningf(nil,
"Skipping tag [%s] (0x%04x) [%s] with an unexpected type: %v ∉ %v", "Skipping tag [%s] (0x%04x) [%s] with an unexpected type: %v ∉ %v",
ii.UnindexedString(), tagId, it.Name, ii.UnindexedString(), tagId, it.Name,
@ -389,18 +446,16 @@ func (ie *IfdEnumerate) parseIfd(ii *exifcommon.IfdIdentity, bp *byteParser, vis
for i := 0; i < int(tagCount); i++ { for i := 0; i < int(tagCount); i++ {
ite, err := ie.parseTag(ii, i, bp) ite, err := ie.parseTag(ii, i, bp)
if err != nil { if err != nil {
if log.Is(err, ErrTagTypeNotValid) == true { if log.Is(err, ErrTagNotFound) == true || log.Is(err, ErrTagTypeNotValid) == true {
// Technically, we have the type on-file in the tags-index, but // These tags should've been fully logged in parseTag(). The
// if the type stored alongside the data disagrees with it, // ITE returned is nil so we can't print anything about them, now.
// which it apparently does, all bets are off.
ifdEnumerateLogger.Warningf(nil, "Tag (0x%04x) in IFD [%s] at position (%d) has invalid type (%d) and will be skipped.", ite.tagId, ii, i, ite.tagType)
continue continue
} }
log.Panic(err) log.Panic(err)
} }
err = ie.postparseTag(ite, med) err = ie.tagPostParse(ite, med)
if err == nil { if err == nil {
if err == ErrTagNotFound { if err == ErrTagNotFound {
continue continue
@ -412,7 +467,7 @@ func (ie *IfdEnumerate) parseIfd(ii *exifcommon.IfdIdentity, bp *byteParser, vis
tagId := ite.TagId() tagId := ite.TagId()
if visitor != nil { if visitor != nil {
err := visitor(ii.String(), ii.Index(), ite) err := visitor(ite)
log.PanicIf(err) log.PanicIf(err)
} }
@ -478,27 +533,43 @@ func (ie *IfdEnumerate) parseIfd(ii *exifcommon.IfdIdentity, bp *byteParser, vis
if enumeratorThumbnailOffset != nil && enumeratorThumbnailSize != nil { if enumeratorThumbnailOffset != nil && enumeratorThumbnailSize != nil {
thumbnailData, err = ie.parseThumbnail(enumeratorThumbnailOffset, enumeratorThumbnailSize) thumbnailData, err = ie.parseThumbnail(enumeratorThumbnailOffset, enumeratorThumbnailSize)
log.PanicIf(err) 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()
// In this case, the value is always an offset. // This this case, the value is always a length.
offset := enumeratorThumbnailOffset.getValueOffset() length := enumeratorThumbnailSize.getValueOffset()
// This this case, the value is always a length. ifdEnumerateLogger.Debugf(nil, "Found thumbnail in IFD [%s]. Its offset is (%d) and is (%d) bytes.", ii, offset, 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
furthestOffset := offset + length if furthestOffset > ie.furthestOffset {
ie.furthestOffset = furthestOffset
if furthestOffset > ie.furthestOffset { }
ie.furthestOffset = furthestOffset
} }
} }
nextIfdOffset, _, err = bp.getUint32() nextIfdOffset, _, err = bp.getUint32()
log.PanicIf(err) log.PanicIf(err)
ifdEnumerateLogger.Debugf(nil, "Next IFD at offset: (%08x)", nextIfdOffset) _, 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 return nextIfdOffset, entries, thumbnailData, nil
} }
@ -587,9 +658,14 @@ func (med *MiscellaneousExifData) UnknownTags() map[exifcommon.BasicTag]exifcomm
return med.unknownTags 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 // Scan enumerates the different EXIF blocks (called IFDs). `rootIfdName` will
// be "IFD" in the TIFF standard. // be "IFD" in the TIFF standard.
func (ie *IfdEnumerate) Scan(iiRoot *exifcommon.IfdIdentity, ifdOffset uint32, visitor TagVisitorFn) (med *MiscellaneousExifData, err error) { func (ie *IfdEnumerate) Scan(iiRoot *exifcommon.IfdIdentity, ifdOffset uint32, visitor TagVisitorFn, so *ScanOptions) (med *MiscellaneousExifData, err error) {
defer func() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
err = log.Wrap(state.(error)) err = log.Wrap(state.(error))
@ -612,38 +688,32 @@ func (ie *IfdEnumerate) Scan(iiRoot *exifcommon.IfdIdentity, ifdOffset uint32, v
// Ifd represents a single, parsed IFD. // Ifd represents a single, parsed IFD.
type Ifd struct { type Ifd struct {
// TODO(dustin): Add NextIfd().
ifdIdentity *exifcommon.IfdIdentity ifdIdentity *exifcommon.IfdIdentity
ByteOrder binary.ByteOrder ifdMapping *exifcommon.IfdMapping
tagIndex *TagIndex
Id int offset uint32
byteOrder binary.ByteOrder
id int
ParentIfd *Ifd parentIfd *Ifd
// ParentTagIndex is our tag position in the parent IFD, if we had a parent // 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 // (if `ParentIfd` is not nil and we weren't an IFD referenced as a sibling
// instead of as a child). // instead of as a child).
ParentTagIndex int parentTagIndex int
Offset uint32 entries []*IfdTagEntry
entriesByTagId map[uint16][]*IfdTagEntry
Entries []*IfdTagEntry children []*Ifd
EntriesByTagId map[uint16][]*IfdTagEntry childIfdIndex map[string]*Ifd
Children []*Ifd
ChildIfdIndex map[string]*Ifd
NextIfdOffset uint32
NextIfd *Ifd
thumbnailData []byte thumbnailData []byte
ifdMapping *exifcommon.IfdMapping nextIfdOffset uint32
tagIndex *TagIndex nextIfd *Ifd
} }
// IfdIdentity returns IFD identity that this struct represents. // IfdIdentity returns IFD identity that this struct represents.
@ -651,6 +721,71 @@ func (ifd *Ifd) IfdIdentity() *exifcommon.IfdIdentity {
return ifd.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 // ChildWithIfdPath returns an `Ifd` struct for the given child of the current
// IFD. // IFD.
func (ifd *Ifd) ChildWithIfdPath(iiChild *exifcommon.IfdIdentity) (childIfd *Ifd, err error) { func (ifd *Ifd) ChildWithIfdPath(iiChild *exifcommon.IfdIdentity) (childIfd *Ifd, err error) {
@ -663,7 +798,7 @@ func (ifd *Ifd) ChildWithIfdPath(iiChild *exifcommon.IfdIdentity) (childIfd *Ifd
// 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. // 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() ifdPath := iiChild.UnindexedString()
for _, childIfd := range ifd.Children { for _, childIfd := range ifd.children {
if childIfd.ifdIdentity.UnindexedString() == ifdPath { if childIfd.ifdIdentity.UnindexedString() == ifdPath {
return childIfd, nil return childIfd, nil
} }
@ -682,7 +817,7 @@ func (ifd *Ifd) FindTagWithId(tagId uint16) (results []*IfdTagEntry, err error)
} }
}() }()
results, found := ifd.EntriesByTagId[tagId] results, found := ifd.entriesByTagId[tagId]
if found != true { if found != true {
log.Panic(ErrTagNotFound) log.Panic(ErrTagNotFound)
} }
@ -707,7 +842,7 @@ func (ifd *Ifd) FindTagWithName(tagName string) (results []*IfdTagEntry, err err
} }
results = make([]*IfdTagEntry, 0) results = make([]*IfdTagEntry, 0)
for _, ite := range ifd.Entries { for _, ite := range ifd.entries {
if ite.TagId() == it.Id { if ite.TagId() == it.Id {
results = append(results, ite) results = append(results, ite)
} }
@ -723,11 +858,11 @@ func (ifd *Ifd) FindTagWithName(tagName string) (results []*IfdTagEntry, err err
// String returns a description string. // String returns a description string.
func (ifd *Ifd) String() string { func (ifd *Ifd) String() string {
parentOffset := uint32(0) parentOffset := uint32(0)
if ifd.ParentIfd != nil { if ifd.parentIfd != nil {
parentOffset = ifd.ParentIfd.Offset parentOffset = ifd.parentIfd.offset
} }
return fmt.Sprintf("Ifd<ID=(%d) IFD-PATH=[%s] INDEX=(%d) COUNT=(%d) OFF=(0x%04x) CHILDREN=(%d) PARENT=(0x%04x) NEXT-IFD=(0x%04x)>", ifd.Id, ifd.ifdIdentity.UnindexedString(), ifd.ifdIdentity.Index(), len(ifd.Entries), ifd.Offset, len(ifd.Children), parentOffset, ifd.NextIfdOffset) return fmt.Sprintf("Ifd<ID=(%d) IFD-PATH=[%s] INDEX=(%d) COUNT=(%d) OFF=(0x%04x) CHILDREN=(%d) PARENT=(0x%04x) NEXT-IFD=(0x%04x)>", ifd.id, ifd.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 // Thumbnail returns the raw thumbnail bytes. This is typically directly
@ -751,14 +886,14 @@ func (ifd *Ifd) dumpTags(tags []*IfdTagEntry) []*IfdTagEntry {
ifdsFoundCount := 0 ifdsFoundCount := 0
for _, ite := range ifd.Entries { for _, ite := range ifd.entries {
tags = append(tags, ite) tags = append(tags, ite)
childIfdPath := ite.ChildIfdPath() childIfdPath := ite.ChildIfdPath()
if childIfdPath != "" { if childIfdPath != "" {
ifdsFoundCount++ ifdsFoundCount++
childIfd, found := ifd.ChildIfdIndex[childIfdPath] childIfd, found := ifd.childIfdIndex[childIfdPath]
if found != true { if found != true {
log.Panicf("alien child IFD referenced by a tag: [%s]", childIfdPath) log.Panicf("alien child IFD referenced by a tag: [%s]", childIfdPath)
} }
@ -767,12 +902,12 @@ func (ifd *Ifd) dumpTags(tags []*IfdTagEntry) []*IfdTagEntry {
} }
} }
if len(ifd.Children) != ifdsFoundCount { if len(ifd.children) != ifdsFoundCount {
log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.Children), ifdsFoundCount) log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.children), ifdsFoundCount)
} }
if ifd.NextIfd != nil { if ifd.nextIfd != nil {
tags = ifd.NextIfd.dumpTags(tags) tags = ifd.nextIfd.dumpTags(tags)
} }
return tags return tags
@ -797,7 +932,7 @@ func (ifd *Ifd) printTagTree(populateValues bool, index, level int, nextLink boo
ifdsFoundCount := 0 ifdsFoundCount := 0
for _, ite := range ifd.Entries { for _, ite := range ifd.entries {
if ite.ChildIfdPath() != "" { if ite.ChildIfdPath() != "" {
fmt.Printf("%s - TAG: %s\n", indent, ite) fmt.Printf("%s - TAG: %s\n", indent, ite)
} else { } else {
@ -841,7 +976,7 @@ func (ifd *Ifd) printTagTree(populateValues bool, index, level int, nextLink boo
if childIfdPath != "" { if childIfdPath != "" {
ifdsFoundCount++ ifdsFoundCount++
childIfd, found := ifd.ChildIfdIndex[childIfdPath] childIfd, found := ifd.childIfdIndex[childIfdPath]
if found != true { if found != true {
log.Panicf("alien child IFD referenced by a tag: [%s]", childIfdPath) log.Panicf("alien child IFD referenced by a tag: [%s]", childIfdPath)
} }
@ -850,12 +985,12 @@ func (ifd *Ifd) printTagTree(populateValues bool, index, level int, nextLink boo
} }
} }
if len(ifd.Children) != ifdsFoundCount { if len(ifd.children) != ifdsFoundCount {
log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.Children), ifdsFoundCount) log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.children), ifdsFoundCount)
} }
if ifd.NextIfd != nil { if ifd.nextIfd != nil {
ifd.NextIfd.printTagTree(populateValues, index+1, level, true) ifd.nextIfd.printTagTree(populateValues, index+1, level, true)
} }
} }
@ -878,12 +1013,12 @@ func (ifd *Ifd) printIfdTree(level int, nextLink bool) {
ifdsFoundCount := 0 ifdsFoundCount := 0
for _, ite := range ifd.Entries { for _, ite := range ifd.entries {
childIfdPath := ite.ChildIfdPath() childIfdPath := ite.ChildIfdPath()
if childIfdPath != "" { if childIfdPath != "" {
ifdsFoundCount++ ifdsFoundCount++
childIfd, found := ifd.ChildIfdIndex[childIfdPath] childIfd, found := ifd.childIfdIndex[childIfdPath]
if found != true { if found != true {
log.Panicf("alien child IFD referenced by a tag: [%s]", childIfdPath) log.Panicf("alien child IFD referenced by a tag: [%s]", childIfdPath)
} }
@ -892,12 +1027,12 @@ func (ifd *Ifd) printIfdTree(level int, nextLink bool) {
} }
} }
if len(ifd.Children) != ifdsFoundCount { if len(ifd.children) != ifdsFoundCount {
log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.Children), ifdsFoundCount) log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.children), ifdsFoundCount)
} }
if ifd.NextIfd != nil { if ifd.nextIfd != nil {
ifd.NextIfd.printIfdTree(level, true) ifd.nextIfd.printIfdTree(level, true)
} }
} }
@ -914,8 +1049,8 @@ func (ifd *Ifd) dumpTree(tagsDump []string, level int) []string {
indent := strings.Repeat(" ", level*2) indent := strings.Repeat(" ", level*2)
var ifdPhrase string var ifdPhrase string
if ifd.ParentIfd != nil { if ifd.parentIfd != nil {
ifdPhrase = fmt.Sprintf("[%s]->[%s]:(%d)", ifd.ParentIfd.ifdIdentity.UnindexedString(), ifd.ifdIdentity.UnindexedString(), ifd.ifdIdentity.Index()) ifdPhrase = fmt.Sprintf("[%s]->[%s]:(%d)", ifd.parentIfd.ifdIdentity.UnindexedString(), ifd.ifdIdentity.UnindexedString(), ifd.ifdIdentity.Index())
} else { } else {
ifdPhrase = fmt.Sprintf("[ROOT]->[%s]:(%d)", ifd.ifdIdentity.UnindexedString(), ifd.ifdIdentity.Index()) ifdPhrase = fmt.Sprintf("[ROOT]->[%s]:(%d)", ifd.ifdIdentity.UnindexedString(), ifd.ifdIdentity.Index())
} }
@ -924,14 +1059,14 @@ func (ifd *Ifd) dumpTree(tagsDump []string, level int) []string {
tagsDump = append(tagsDump, startBlurb) tagsDump = append(tagsDump, startBlurb)
ifdsFoundCount := 0 ifdsFoundCount := 0
for _, ite := range ifd.Entries { for _, ite := range ifd.entries {
tagsDump = append(tagsDump, fmt.Sprintf("%s - (0x%04x)", indent, ite.TagId())) tagsDump = append(tagsDump, fmt.Sprintf("%s - (0x%04x)", indent, ite.TagId()))
childIfdPath := ite.ChildIfdPath() childIfdPath := ite.ChildIfdPath()
if childIfdPath != "" { if childIfdPath != "" {
ifdsFoundCount++ ifdsFoundCount++
childIfd, found := ifd.ChildIfdIndex[childIfdPath] childIfd, found := ifd.childIfdIndex[childIfdPath]
if found != true { if found != true {
log.Panicf("alien child IFD referenced by a tag: [%s]", childIfdPath) log.Panicf("alien child IFD referenced by a tag: [%s]", childIfdPath)
} }
@ -940,18 +1075,18 @@ func (ifd *Ifd) dumpTree(tagsDump []string, level int) []string {
} }
} }
if len(ifd.Children) != ifdsFoundCount { if len(ifd.children) != ifdsFoundCount {
log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", 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) finishBlurb := fmt.Sprintf("%s< IFD %s BOTTOM", indent, ifdPhrase)
tagsDump = append(tagsDump, finishBlurb) tagsDump = append(tagsDump, finishBlurb)
if ifd.NextIfd != nil { if ifd.nextIfd != nil {
siblingBlurb := fmt.Sprintf("%s* LINKING TO SIBLING IFD [%s]:(%d)", indent, ifd.NextIfd.ifdIdentity.UnindexedString(), ifd.NextIfd.ifdIdentity.Index()) siblingBlurb := fmt.Sprintf("%s* LINKING TO SIBLING IFD [%s]:(%d)", indent, ifd.nextIfd.ifdIdentity.UnindexedString(), ifd.nextIfd.ifdIdentity.Index())
tagsDump = append(tagsDump, siblingBlurb) tagsDump = append(tagsDump, siblingBlurb)
tagsDump = ifd.NextIfd.dumpTree(tagsDump, level) tagsDump = ifd.nextIfd.dumpTree(tagsDump, level)
} }
return tagsDump return tagsDump
@ -973,11 +1108,11 @@ func (ifd *Ifd) GpsInfo() (gi *GpsInfo, err error) {
gi = new(GpsInfo) gi = new(GpsInfo)
if ifd.ifdIdentity.UnindexedString() != exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString() { if ifd.ifdIdentity.Equals(exifcommon.IfdGpsInfoStandardIfdIdentity) == false {
log.Panicf("GPS can only be read on GPS IFD: [%s] != [%s]", ifd.ifdIdentity.UnindexedString(), exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString()) log.Panicf("GPS can only be read on GPS IFD: [%s]", ifd.ifdIdentity.UnindexedString())
} }
if tags, found := ifd.EntriesByTagId[TagGpsVersionId]; found == false { if tags, found := ifd.entriesByTagId[TagGpsVersionId]; found == false {
// We've seen this. We'll just have to default to assuming we're in a // We've seen this. We'll just have to default to assuming we're in a
// 2.2.0.0 format. // 2.2.0.0 format.
ifdEnumerateLogger.Warningf(nil, "No GPS version tag (0x%04x) found.", TagGpsVersionId) ifdEnumerateLogger.Warningf(nil, "No GPS version tag (0x%04x) found.", TagGpsVersionId)
@ -999,7 +1134,7 @@ func (ifd *Ifd) GpsInfo() (gi *GpsInfo, err error) {
} }
} }
tags, found := ifd.EntriesByTagId[TagLatitudeId] tags, found := ifd.entriesByTagId[TagLatitudeId]
if found == false { if found == false {
ifdEnumerateLogger.Warningf(nil, "latitude not found") ifdEnumerateLogger.Warningf(nil, "latitude not found")
log.Panic(ErrNoGpsTags) log.Panic(ErrNoGpsTags)
@ -1009,7 +1144,7 @@ func (ifd *Ifd) GpsInfo() (gi *GpsInfo, err error) {
log.PanicIf(err) log.PanicIf(err)
// Look for whether North or South. // Look for whether North or South.
tags, found = ifd.EntriesByTagId[TagLatitudeRefId] tags, found = ifd.entriesByTagId[TagLatitudeRefId]
if found == false { if found == false {
ifdEnumerateLogger.Warningf(nil, "latitude-ref not found") ifdEnumerateLogger.Warningf(nil, "latitude-ref not found")
log.Panic(ErrNoGpsTags) log.Panic(ErrNoGpsTags)
@ -1018,7 +1153,7 @@ func (ifd *Ifd) GpsInfo() (gi *GpsInfo, err error) {
latitudeRefValue, err := tags[0].Value() latitudeRefValue, err := tags[0].Value()
log.PanicIf(err) log.PanicIf(err)
tags, found = ifd.EntriesByTagId[TagLongitudeId] tags, found = ifd.entriesByTagId[TagLongitudeId]
if found == false { if found == false {
ifdEnumerateLogger.Warningf(nil, "longitude not found") ifdEnumerateLogger.Warningf(nil, "longitude not found")
log.Panic(ErrNoGpsTags) log.Panic(ErrNoGpsTags)
@ -1028,7 +1163,7 @@ func (ifd *Ifd) GpsInfo() (gi *GpsInfo, err error) {
log.PanicIf(err) log.PanicIf(err)
// Look for whether West or East. // Look for whether West or East.
tags, found = ifd.EntriesByTagId[TagLongitudeRefId] tags, found = ifd.entriesByTagId[TagLongitudeRefId]
if found == false { if found == false {
ifdEnumerateLogger.Warningf(nil, "longitude-ref not found") ifdEnumerateLogger.Warningf(nil, "longitude-ref not found")
log.Panic(ErrNoGpsTags) log.Panic(ErrNoGpsTags)
@ -1051,8 +1186,8 @@ func (ifd *Ifd) GpsInfo() (gi *GpsInfo, err error) {
// Parse altitude. // Parse altitude.
altitudeTags, foundAltitude := ifd.EntriesByTagId[TagAltitudeId] altitudeTags, foundAltitude := ifd.entriesByTagId[TagAltitudeId]
altitudeRefTags, foundAltitudeRef := ifd.EntriesByTagId[TagAltitudeRefId] altitudeRefTags, foundAltitudeRef := ifd.entriesByTagId[TagAltitudeRefId]
if foundAltitude == true && foundAltitudeRef == true { if foundAltitude == true && foundAltitudeRef == true {
altitudePhrase, err := altitudeTags[0].Format() altitudePhrase, err := altitudeTags[0].Format()
@ -1085,8 +1220,8 @@ func (ifd *Ifd) GpsInfo() (gi *GpsInfo, err error) {
// Parse timestamp from separate date and time tags. // Parse timestamp from separate date and time tags.
timestampTags, foundTimestamp := ifd.EntriesByTagId[TagTimestampId] timestampTags, foundTimestamp := ifd.entriesByTagId[TagTimestampId]
datestampTags, foundDatestamp := ifd.EntriesByTagId[TagDatestampId] datestampTags, foundDatestamp := ifd.entriesByTagId[TagDatestampId]
if foundTimestamp == true && foundDatestamp == true { if foundTimestamp == true && foundDatestamp == true {
datestampValue, err := datestampTags[0].Value() datestampValue, err := datestampTags[0].Value()
@ -1139,11 +1274,11 @@ func (ifd *Ifd) EnumerateTagsRecursively(visitor ParsedTagVisitor) (err error) {
} }
}() }()
for ptr := ifd; ptr != nil; ptr = ptr.NextIfd { for ptr := ifd; ptr != nil; ptr = ptr.nextIfd {
for _, ite := range ifd.Entries { for _, ite := range ifd.entries {
childIfdPath := ite.ChildIfdPath() childIfdPath := ite.ChildIfdPath()
if childIfdPath != "" { if childIfdPath != "" {
childIfd := ifd.ChildIfdIndex[childIfdPath] childIfd := ifd.childIfdIndex[childIfdPath]
err := childIfd.EnumerateTagsRecursively(visitor) err := childIfd.EnumerateTagsRecursively(visitor)
log.PanicIf(err) log.PanicIf(err)
@ -1254,21 +1389,21 @@ func (ie *IfdEnumerate) Collect(rootIfdOffset uint32) (index IfdIndex, err error
ifd := &Ifd{ ifd := &Ifd{
ifdIdentity: ii, ifdIdentity: ii,
ByteOrder: ie.byteOrder, byteOrder: ie.byteOrder,
Id: id, id: id,
ParentIfd: parentIfd, parentIfd: parentIfd,
ParentTagIndex: qi.ParentTagIndex, parentTagIndex: qi.ParentTagIndex,
Offset: offset, offset: offset,
Entries: entries, entries: entries,
EntriesByTagId: entriesByTagId, entriesByTagId: entriesByTagId,
// This is populated as each child is processed. // This is populated as each child is processed.
Children: make([]*Ifd, 0), children: make([]*Ifd, 0),
NextIfdOffset: nextIfdOffset, nextIfdOffset: nextIfdOffset,
thumbnailData: thumbnailData, thumbnailData: thumbnailData,
ifdMapping: ie.ifdMapping, ifdMapping: ie.ifdMapping,
@ -1286,13 +1421,13 @@ func (ie *IfdEnumerate) Collect(rootIfdOffset uint32) (index IfdIndex, err error
// Add a link from the previous IFD in the chain to us. // Add a link from the previous IFD in the chain to us.
if previousIfd, found := edges[offset]; found == true { if previousIfd, found := edges[offset]; found == true {
previousIfd.NextIfd = ifd previousIfd.nextIfd = ifd
} }
// Attach as a child to our parent (where we appeared as a tag in // Attach as a child to our parent (where we appeared as a tag in
// that IFD). // that IFD).
if parentIfd != nil { if parentIfd != nil {
parentIfd.Children = append(parentIfd.Children, ifd) parentIfd.children = append(parentIfd.children, ifd)
} }
// Determine if any of our entries is a child IFD and queue it. // Determine if any of our entries is a child IFD and queue it.
@ -1362,13 +1497,13 @@ func (ie *IfdEnumerate) setChildrenIndex(ifd *Ifd) (err error) {
}() }()
childIfdIndex := make(map[string]*Ifd) childIfdIndex := make(map[string]*Ifd)
for _, childIfd := range ifd.Children { for _, childIfd := range ifd.children {
childIfdIndex[childIfd.ifdIdentity.UnindexedString()] = childIfd childIfdIndex[childIfd.ifdIdentity.UnindexedString()] = childIfd
} }
ifd.ChildIfdIndex = childIfdIndex ifd.childIfdIndex = childIfdIndex
for _, childIfd := range ifd.Children { for _, childIfd := range ifd.children {
err := ie.setChildrenIndex(childIfd) err := ie.setChildrenIndex(childIfd)
log.PanicIf(err) log.PanicIf(err)
} }
@ -1391,21 +1526,26 @@ func (ie *IfdEnumerate) FurthestOffset() uint32 {
return ie.furthestOffset return ie.furthestOffset
} }
// ParseOneIfd is a hack to use an IE to parse a raw IFD block. Can be used for // 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 // 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 // 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 // the proper number (if its actually a sibling to the first child, for
// instance). // instance).
func ParseOneIfd(ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, ii *exifcommon.IfdIdentity, byteOrder binary.ByteOrder, ifdBlock []byte, visitor TagVisitorFn) (nextIfdOffset uint32, entries []*IfdTagEntry, err error) { 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() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
err = log.Wrap(state.(error)) err = log.Wrap(state.(error))
} }
}() }()
ie := NewIfdEnumerate(ifdMapping, tagIndex, make([]byte, 0), byteOrder) // TODO(dustin): Add test
bp, err := newByteParser(ifdBlock, byteOrder, 0) ebs := NewExifReadSeekerWithBytes(ifdBlock)
rs, err := ebs.GetReadSeeker(0)
log.PanicIf(err)
bp, err := newByteParser(rs, byteOrder, 0)
if err != nil { if err != nil {
if err == ErrOffsetInvalid { if err == ErrOffsetInvalid {
return 0, nil, err return 0, nil, err
@ -1414,23 +1554,31 @@ func ParseOneIfd(ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, ii *exif
log.Panic(err) log.Panic(err)
} }
dummyEbs := NewExifReadSeekerWithBytes([]byte{})
ie := NewIfdEnumerate(ifdMapping, tagIndex, dummyEbs, byteOrder)
nextIfdOffset, entries, _, err = ie.parseIfd(ii, bp, visitor, true, nil) nextIfdOffset, entries, _, err = ie.parseIfd(ii, bp, visitor, true, nil)
log.PanicIf(err) log.PanicIf(err)
return nextIfdOffset, entries, nil return nextIfdOffset, entries, nil
} }
// ParseOneTag is a hack to use an IE to parse a raw tag block. // 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) { func parseOneTag(ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, ii *exifcommon.IfdIdentity, byteOrder binary.ByteOrder, tagBlock []byte) (ite *IfdTagEntry, err error) {
defer func() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
err = log.Wrap(state.(error)) err = log.Wrap(state.(error))
} }
}() }()
ie := NewIfdEnumerate(ifdMapping, tagIndex, make([]byte, 0), byteOrder) // TODO(dustin): Add test
bp, err := newByteParser(tagBlock, byteOrder, 0) ebs := NewExifReadSeekerWithBytes(tagBlock)
rs, err := ebs.GetReadSeeker(0)
log.PanicIf(err)
bp, err := newByteParser(rs, byteOrder, 0)
if err != nil { if err != nil {
if err == ErrOffsetInvalid { if err == ErrOffsetInvalid {
return nil, err return nil, err
@ -1439,10 +1587,13 @@ func ParseOneTag(ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, ii *exif
log.Panic(err) log.Panic(err)
} }
dummyEbs := NewExifReadSeekerWithBytes([]byte{})
ie := NewIfdEnumerate(ifdMapping, tagIndex, dummyEbs, byteOrder)
ite, err = ie.parseTag(ii, 0, bp) ite, err = ie.parseTag(ii, 0, bp)
log.PanicIf(err) log.PanicIf(err)
err = ie.postparseTag(ite, nil) err = ie.tagPostParse(ite, nil)
if err != nil { if err != nil {
if err == ErrTagNotFound { if err == ErrTagNotFound {
return nil, err return nil, err
@ -1484,16 +1635,16 @@ func FindIfdFromRootIfd(rootIfd *Ifd, ifdPath string) (ifd *Ifd, err error) {
// 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. // 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 thisIfd := rootIfd
for currentRootIndex := 0; currentRootIndex < desiredRootIndex; currentRootIndex++ { for currentRootIndex := 0; currentRootIndex < desiredRootIndex; currentRootIndex++ {
if thisIfd.NextIfd == nil { if thisIfd.nextIfd == nil {
log.Panicf("Root-IFD index (%d) does not exist in the data.", currentRootIndex) log.Panicf("Root-IFD index (%d) does not exist in the data.", currentRootIndex)
} }
thisIfd = thisIfd.NextIfd thisIfd = thisIfd.nextIfd
} }
for _, itii := range lineage { for _, itii := range lineage {
var hit *Ifd var hit *Ifd
for _, childIfd := range thisIfd.Children { for _, childIfd := range thisIfd.children {
if childIfd.ifdIdentity.TagId() == itii.TagId { if childIfd.ifdIdentity.TagId() == itii.TagId {
hit = childIfd hit = childIfd
break break
@ -1502,18 +1653,18 @@ func FindIfdFromRootIfd(rootIfd *Ifd, ifdPath string) (ifd *Ifd, err error) {
// If we didn't find the child, add it. // If we didn't find the child, add it.
if hit == nil { if hit == nil {
log.Panicf("IFD [%s] in [%s] not found: %s", itii.Name, ifdPath, thisIfd.Children) log.Panicf("IFD [%s] in [%s] not found: %s", itii.Name, ifdPath, thisIfd.children)
} }
thisIfd = hit thisIfd = hit
// If we didn't find the sibling, add it. // If we didn't find the sibling, add it.
for i := 0; i < itii.Index; i++ { for i := 0; i < itii.Index; i++ {
if thisIfd.NextIfd == nil { if thisIfd.nextIfd == nil {
log.Panicf("IFD [%s] does not have (%d) occurrences/siblings", thisIfd.ifdIdentity.UnindexedString(), itii.Index) log.Panicf("IFD [%s] does not have (%d) occurrences/siblings", thisIfd.ifdIdentity.UnindexedString(), itii.Index)
} }
thisIfd = thisIfd.NextIfd thisIfd = thisIfd.nextIfd
} }
} }

View file

@ -2,13 +2,14 @@ package exif
import ( import (
"fmt" "fmt"
"io"
"encoding/binary" "encoding/binary"
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
"github.com/dsoprea/go-exif/v2/undefined" "github.com/dsoprea/go-exif/v3/undefined"
) )
var ( var (
@ -42,23 +43,23 @@ type IfdTagEntry struct {
isUnhandledUnknown bool isUnhandledUnknown bool
addressableData []byte rs io.ReadSeeker
byteOrder binary.ByteOrder byteOrder binary.ByteOrder
tagName string tagName string
} }
func newIfdTagEntry(ii *exifcommon.IfdIdentity, tagId uint16, tagIndex int, tagType exifcommon.TagTypePrimitive, unitCount uint32, valueOffset uint32, rawValueOffset []byte, addressableData []byte, byteOrder binary.ByteOrder) *IfdTagEntry { 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{ return &IfdTagEntry{
ifdIdentity: ii, ifdIdentity: ii,
tagId: tagId, tagId: tagId,
tagIndex: tagIndex, tagIndex: tagIndex,
tagType: tagType, tagType: tagType,
unitCount: unitCount, unitCount: unitCount,
valueOffset: valueOffset, valueOffset: valueOffset,
rawValueOffset: rawValueOffset, rawValueOffset: rawValueOffset,
addressableData: addressableData, rs: rs,
byteOrder: byteOrder, byteOrder: byteOrder,
} }
} }
@ -291,7 +292,7 @@ func (ite *IfdTagEntry) getValueContext() *exifcommon.ValueContext {
ite.unitCount, ite.unitCount,
ite.valueOffset, ite.valueOffset,
ite.rawValueOffset, ite.rawValueOffset,
ite.addressableData, ite.rs,
ite.tagType, ite.tagType,
ite.byteOrder) ite.byteOrder)
} }

View file

@ -2,11 +2,12 @@ package exif
import ( import (
"fmt" "fmt"
"sync"
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
const ( const (
@ -114,7 +115,7 @@ func (it *IndexedTag) Is(ifdPath string, id uint16) bool {
// GetEncodingType returns the largest type that this tag's value can occupy. // GetEncodingType returns the largest type that this tag's value can occupy.
func (it *IndexedTag) GetEncodingType(value interface{}) exifcommon.TagTypePrimitive { func (it *IndexedTag) GetEncodingType(value interface{}) exifcommon.TagTypePrimitive {
// For convenience, we handle encoding a `time.Time` directly. // For convenience, we handle encoding a `time.Time` directly.
if IsTime(value) == true { if exifcommon.IsTime(value) == true {
// Timestamps are encoded as ASCII. // Timestamps are encoded as ASCII.
value = "" value = ""
} }
@ -177,6 +178,10 @@ func (it *IndexedTag) DoesSupportType(tagType exifcommon.TagTypePrimitive) bool
type TagIndex struct { type TagIndex struct {
tagsByIfd map[string]map[uint16]*IndexedTag tagsByIfd map[string]map[uint16]*IndexedTag
tagsByIfdR map[string]map[string]*IndexedTag tagsByIfdR map[string]map[string]*IndexedTag
mutex sync.Mutex
doUniversalSearch bool
} }
// NewTagIndex returns a new TagIndex struct. // NewTagIndex returns a new TagIndex struct.
@ -189,6 +194,16 @@ func NewTagIndex() *TagIndex {
return ti 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. // Add registers a new tag to be recognized during the parse.
func (ti *TagIndex) Add(it *IndexedTag) (err error) { func (ti *TagIndex) Add(it *IndexedTag) (err error) {
defer func() { defer func() {
@ -197,6 +212,9 @@ func (ti *TagIndex) Add(it *IndexedTag) (err error) {
} }
}() }()
ti.mutex.Lock()
defer ti.mutex.Unlock()
// Store by ID. // Store by ID.
family, found := ti.tagsByIfd[it.IfdPath] family, found := ti.tagsByIfd[it.IfdPath]
@ -228,9 +246,7 @@ func (ti *TagIndex) Add(it *IndexedTag) (err error) {
return nil return nil
} }
// Get returns information about the non-IFD tag given a tag ID. `ifdPath` must func (ti *TagIndex) getOne(ifdPath string, id uint16) (it *IndexedTag, err error) {
// not be fully-qualified.
func (ti *TagIndex) Get(ii *exifcommon.IfdIdentity, id uint16) (it *IndexedTag, err error) {
defer func() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
err = log.Wrap(state.(error)) err = log.Wrap(state.(error))
@ -242,7 +258,8 @@ func (ti *TagIndex) Get(ii *exifcommon.IfdIdentity, id uint16) (it *IndexedTag,
log.PanicIf(err) log.PanicIf(err)
} }
ifdPath := ii.UnindexedString() ti.mutex.Lock()
defer ti.mutex.Unlock()
family, found := ti.tagsByIfd[ifdPath] family, found := ti.tagsByIfd[ifdPath]
if found == false { if found == false {
@ -257,6 +274,53 @@ func (ti *TagIndex) Get(ii *exifcommon.IfdIdentity, id uint16) (it *IndexedTag,
return it, nil 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 ( var (
// tagGuessDefaultIfdIdentities describes which IFDs we'll look for a given // 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 // tag-ID in, if it's not found where it's supposed to be. We suppose that

View file

@ -59,6 +59,15 @@ IFD/Exif:
- id: 0x9004 - id: 0x9004
name: DateTimeDigitized name: DateTimeDigitized
type_name: ASCII 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 - id: 0x9101
name: ComponentsConfiguration name: ComponentsConfiguration
type_name: UNDEFINED type_name: UNDEFINED
@ -909,6 +918,36 @@ IFD:
- id: 0xc74e - id: 0xc74e
name: OpcodeList3 name: OpcodeList3
type_name: UNDEFINED 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: IFD/Exif/Iop:
- id: 0x0001 - id: 0x0001
name: InteroperabilityIndex name: InteroperabilityIndex

View file

@ -9,7 +9,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
var ( var (
@ -24,9 +24,9 @@ func getExifSimpleTestIb() *IfdBuilder {
} }
}() }()
im := NewIfdMapping() im := exifcommon.NewIfdMapping()
err := LoadStandardIfds(im) err := exifcommon.LoadStandardIfds(im)
log.PanicIf(err) log.PanicIf(err)
ti := NewTagIndex() ti := NewTagIndex()
@ -55,9 +55,9 @@ func getExifSimpleTestIbBytes() []byte {
} }
}() }()
im := NewIfdMapping() im := exifcommon.NewIfdMapping()
err := LoadStandardIfds(im) err := exifcommon.LoadStandardIfds(im)
log.PanicIf(err) log.PanicIf(err)
ti := NewTagIndex() ti := NewTagIndex()
@ -91,9 +91,9 @@ func validateExifSimpleTestIb(exifData []byte, t *testing.T) {
} }
}() }()
im := NewIfdMapping() im := exifcommon.NewIfdMapping()
err := LoadStandardIfds(im) err := exifcommon.LoadStandardIfds(im)
log.PanicIf(err) log.PanicIf(err)
ti := NewTagIndex() ti := NewTagIndex()
@ -113,19 +113,19 @@ func validateExifSimpleTestIb(exifData []byte, t *testing.T) {
ifd := index.RootIfd ifd := index.RootIfd
if ifd.ByteOrder != exifcommon.TestDefaultByteOrder { if ifd.ByteOrder() != exifcommon.TestDefaultByteOrder {
t.Fatalf("IFD byte-order not correct.") t.Fatalf("IFD byte-order not correct.")
} else if ifd.ifdIdentity.UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() { } else if ifd.ifdIdentity.UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() {
t.Fatalf("IFD name not correct.") t.Fatalf("IFD name not correct.")
} else if ifd.ifdIdentity.Index() != 0 { } else if ifd.ifdIdentity.Index() != 0 {
t.Fatalf("IFD index not zero: (%d)", ifd.ifdIdentity.Index()) t.Fatalf("IFD index not zero: (%d)", ifd.ifdIdentity.Index())
} else if ifd.Offset != uint32(0x0008) { } else if ifd.Offset() != uint32(0x0008) {
t.Fatalf("IFD offset not correct.") t.Fatalf("IFD offset not correct.")
} else if len(ifd.Entries) != 4 { } else if len(ifd.Entries()) != 4 {
t.Fatalf("IFD number of entries not correct: (%d)", len(ifd.Entries)) t.Fatalf("IFD number of entries not correct: (%d)", len(ifd.Entries()))
} else if ifd.NextIfdOffset != uint32(0) { } else if ifd.nextIfdOffset != uint32(0) {
t.Fatalf("Next-IFD offset is non-zero.") t.Fatalf("Next-IFD offset is non-zero.")
} else if ifd.NextIfd != nil { } else if ifd.nextIfd != nil {
t.Fatalf("Next-IFD pointer is non-nil.") t.Fatalf("Next-IFD pointer is non-nil.")
} }
@ -141,7 +141,7 @@ func validateExifSimpleTestIb(exifData []byte, t *testing.T) {
{tagId: 0x013e, value: []exifcommon.Rational{{Numerator: 0x11112222, Denominator: 0x33334444}}}, {tagId: 0x013e, value: []exifcommon.Rational{{Numerator: 0x11112222, Denominator: 0x33334444}}},
} }
for i, ite := range ifd.Entries { for i, ite := range ifd.Entries() {
if ite.TagId() != expected[i].tagId { 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) t.Fatalf("Tag-ID for entry (%d) not correct: (0x%02x) != (0x%02x)", i, ite.TagId(), expected[i].tagId)
} }
@ -180,3 +180,9 @@ func getTestGpsImageFilepath() string {
testGpsImageFilepath := path.Join(assetsPath, "gps.jpg") testGpsImageFilepath := path.Join(assetsPath, "gps.jpg")
return testGpsImageFilepath return testGpsImageFilepath
} }
func getTestGeotiffFilepath() string {
assetsPath := exifcommon.GetTestAssetsPath()
testGeotiffFilepath := path.Join(assetsPath, "geotiff_example.tif")
return testGeotiffFilepath
}

View file

@ -5,7 +5,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
// Encode encodes the given encodeable undefined value to bytes. // Encode encodes the given encodeable undefined value to bytes.

View file

@ -8,7 +8,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
type Tag8828Oecf struct { type Tag8828Oecf struct {

View file

@ -5,7 +5,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
type Tag9000ExifVersion struct { type Tag9000ExifVersion struct {

View file

@ -8,7 +8,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
const ( const (

View file

@ -9,7 +9,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
type Tag927CMakerNote struct { type Tag927CMakerNote struct {

View file

@ -8,7 +8,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
var ( var (

View file

@ -5,7 +5,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
type TagA000FlashpixVersion struct { type TagA000FlashpixVersion struct {

View file

@ -8,7 +8,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
type TagA20CSpatialFrequencyResponse struct { type TagA20CSpatialFrequencyResponse struct {

View file

@ -7,7 +7,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
type TagExifA300FileSource uint32 type TagExifA300FileSource uint32

View file

@ -7,7 +7,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
type TagExifA301SceneType uint32 type TagExifA301SceneType uint32

View file

@ -8,7 +8,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
type TagA302CfaPattern struct { type TagA302CfaPattern struct {

View file

@ -5,7 +5,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
type Tag0002InteropVersion struct { type Tag0002InteropVersion struct {

View file

@ -5,7 +5,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
type Tag001BGPSProcessingMethod struct { type Tag001BGPSProcessingMethod struct {

View file

@ -5,7 +5,7 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
type Tag001CGPSAreaInformation struct { type Tag001CGPSAreaInformation struct {

View file

@ -5,7 +5,7 @@ import (
"encoding/binary" "encoding/binary"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
) )
const ( const (

View file

@ -2,86 +2,20 @@ package exif
import ( import (
"fmt" "fmt"
"io"
"math" "math"
"reflect"
"strconv"
"strings"
"time"
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-utility/v2/filesystem"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v3/common"
"github.com/dsoprea/go-exif/v2/undefined" "github.com/dsoprea/go-exif/v3/undefined"
) )
var ( var (
utilityLogger = log.NewLogger("exif.utility") utilityLogger = log.NewLogger("exif.utility")
) )
var (
timeType = reflect.TypeOf(time.Time{})
)
// 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
}
// 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) {
return exifcommon.ExifFullTimestampString(t)
}
// ExifTag is one simple representation of a tag in a flat list of all of them. // ExifTag is one simple representation of a tag in a flat list of all of them.
type ExifTag struct { type ExifTag struct {
// IfdPath is the fully-qualified IFD path (even though it is not named as // IfdPath is the fully-qualified IFD path (even though it is not named as
@ -137,24 +71,93 @@ func (et ExifTag) String() string {
} }
// GetFlatExifData returns a simple, flat representation of all tags. // GetFlatExifData returns a simple, flat representation of all tags.
func GetFlatExifData(exifData []byte) (exifTags []ExifTag, err error) { func GetFlatExifData(exifData []byte, so *ScanOptions) (exifTags []ExifTag, med *MiscellaneousExifData, err error) {
defer func() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
err = log.Wrap(state.(error)) err = log.Wrap(state.(error))
} }
}() }()
eh, err := ParseExifHeader(exifData) 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) log.PanicIf(err)
im := NewIfdMappingWithStandard()
ti := NewTagIndex() ti := NewTagIndex()
ie := NewIfdEnumerate(im, ti, exifData, eh.ByteOrder) if doUniversalSearch == true {
ti.SetUniversalSearch(true)
}
ebs := NewExifReadSeeker(rs)
ie := NewIfdEnumerate(im, ti, ebs, eh.ByteOrder)
exifTags = make([]ExifTag, 0) exifTags = make([]ExifTag, 0)
visitor := func(fqIfdPath string, ifdIndex int, ite *IfdTagEntry) (err error) { visitor := func(ite *IfdTagEntry) (err error) {
// This encodes down to base64. Since this an example tool and we do not // 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 // expect to ever decode the output, we are not worried about
// specifically base64-encoding it in order to have a measure of // specifically base64-encoding it in order to have a measure of
@ -172,13 +175,19 @@ func GetFlatExifData(exifData []byte) (exifTags []ExifTag, err error) {
if err != nil { if err != nil {
if err == exifcommon.ErrUnhandledUndefinedTypedTag { if err == exifcommon.ErrUnhandledUndefinedTypedTag {
value = exifundefined.UnparseableUnknownTagValuePlaceholder 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 { } else {
log.Panic(err) log.Panic(err)
} }
} }
et := ExifTag{ et := ExifTag{
IfdPath: fqIfdPath, IfdPath: ite.IfdPath(),
TagId: ite.TagId(), TagId: ite.TagId(),
TagName: ite.TagName(), TagName: ite.TagName(),
UnitCount: ite.UnitCount(), UnitCount: ite.UnitCount(),
@ -200,10 +209,10 @@ func GetFlatExifData(exifData []byte) (exifTags []ExifTag, err error) {
return nil return nil
} }
_, err = ie.Scan(exifcommon.IfdStandardIfdIdentity, eh.FirstIfdOffset, visitor) med, err = ie.Scan(exifcommon.IfdStandardIfdIdentity, eh.FirstIfdOffset, visitor, nil)
log.PanicIf(err) log.PanicIf(err)
return exifTags, nil return exifTags, med, nil
} }
// GpsDegreesEquals returns true if the two `GpsDegrees` are identical. // GpsDegreesEquals returns true if the two `GpsDegrees` are identical.
@ -226,8 +235,3 @@ func GpsDegreesEquals(gi1, gi2 GpsDegrees) bool {
return true return true
} }
// IsTime returns true if the value is a `time.Time`.
func IsTime(v interface{}) bool {
return reflect.TypeOf(v) == timeType
}

View file

@ -1,367 +0,0 @@
package exif
import (
"encoding/binary"
"github.com/dsoprea/go-logging"
)
var (
parser *Parser
)
// ValueContext describes all of the parameters required to find and extract
// the actual tag value.
type ValueContext struct {
unitCount uint32
valueOffset uint32
rawValueOffset []byte
addressableData []byte
tagType TagTypePrimitive
byteOrder binary.ByteOrder
// undefinedValueTagType is the effective type to use if this is an
// "undefined" value.
undefinedValueTagType TagTypePrimitive
ifdPath string
tagId uint16
}
func newValueContext(ifdPath string, tagId uint16, unitCount, valueOffset uint32, rawValueOffset, addressableData []byte, tagType TagTypePrimitive, byteOrder binary.ByteOrder) *ValueContext {
return &ValueContext{
unitCount: unitCount,
valueOffset: valueOffset,
rawValueOffset: rawValueOffset,
addressableData: addressableData,
tagType: tagType,
byteOrder: byteOrder,
ifdPath: ifdPath,
tagId: tagId,
}
}
func newValueContextFromTag(ite *IfdTagEntry, addressableData []byte, byteOrder binary.ByteOrder) *ValueContext {
return newValueContext(
ite.IfdPath,
ite.TagId,
ite.UnitCount,
ite.ValueOffset,
ite.RawValueOffset,
addressableData,
ite.TagType,
byteOrder)
}
func (vc *ValueContext) SetUnknownValueType(tagType TagTypePrimitive) {
vc.undefinedValueTagType = tagType
}
func (vc *ValueContext) UnitCount() uint32 {
return vc.unitCount
}
func (vc *ValueContext) ValueOffset() uint32 {
return vc.valueOffset
}
func (vc *ValueContext) RawValueOffset() []byte {
return vc.rawValueOffset
}
func (vc *ValueContext) AddressableData() []byte {
return vc.addressableData
}
// isEmbedded returns whether the value is embedded or a reference. This can't
// be precalculated since the size is not defined for all types (namely the
// "undefined" types).
func (vc *ValueContext) isEmbedded() bool {
tagType := vc.effectiveValueType()
return (tagType.Size() * int(vc.unitCount)) <= 4
}
func (vc *ValueContext) effectiveValueType() (tagType TagTypePrimitive) {
if vc.tagType == TypeUndefined {
tagType = vc.undefinedValueTagType
if tagType == 0 {
log.Panicf("undefined-value type not set")
}
} else {
tagType = vc.tagType
}
return tagType
}
func (vc *ValueContext) readRawEncoded() (rawBytes []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
tagType := vc.effectiveValueType()
unitSizeRaw := uint32(tagType.Size())
if vc.isEmbedded() == true {
byteLength := unitSizeRaw * vc.unitCount
return vc.rawValueOffset[:byteLength], nil
} else {
return vc.addressableData[vc.valueOffset : vc.valueOffset+vc.unitCount*unitSizeRaw], nil
}
}
// Format returns a string representation for the value.
//
// Where the type is not ASCII, `justFirst` indicates whether to just stringify
// the first item in the slice (or return an empty string if the slice is
// empty).
//
// Since this method lacks the information to process undefined-type tags (e.g.
// byte-order, tag-ID, IFD type), it will return an error if attempted. See
// `Undefined()`.
func (vc *ValueContext) Format() (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawBytes, err := vc.readRawEncoded()
log.PanicIf(err)
phrase, err := Format(rawBytes, vc.tagType, false, vc.byteOrder)
log.PanicIf(err)
return phrase, nil
}
// FormatOne is similar to `Format` but only gets and stringifies the first
// item.
func (vc *ValueContext) FormatFirst() (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawBytes, err := vc.readRawEncoded()
log.PanicIf(err)
phrase, err := Format(rawBytes, vc.tagType, true, vc.byteOrder)
log.PanicIf(err)
return phrase, nil
}
func (vc *ValueContext) ReadBytes() (value []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseBytes(rawValue, vc.unitCount)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadAscii() (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseAscii(rawValue, vc.unitCount)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadAsciiNoNul() (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseAsciiNoNul(rawValue, vc.unitCount)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadShorts() (value []uint16, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseShorts(rawValue, vc.unitCount, vc.byteOrder)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadLongs() (value []uint32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseLongs(rawValue, vc.unitCount, vc.byteOrder)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadRationals() (value []Rational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseRationals(rawValue, vc.unitCount, vc.byteOrder)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadSignedLongs() (value []int32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseSignedLongs(rawValue, vc.unitCount, vc.byteOrder)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadSignedRationals() (value []SignedRational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseSignedRationals(rawValue, vc.unitCount, vc.byteOrder)
log.PanicIf(err)
return value, nil
}
// Values knows how to resolve the given value. This value is always a list
// (undefined-values aside), so we're named accordingly.
//
// Since this method lacks the information to process unknown-type tags (e.g.
// byte-order, tag-ID, IFD type), it will return an error if attempted. See
// `Undefined()`.
func (vc *ValueContext) Values() (values interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if vc.tagType == TypeByte {
values, err = vc.ReadBytes()
log.PanicIf(err)
} else if vc.tagType == TypeAscii {
values, err = vc.ReadAscii()
log.PanicIf(err)
} else if vc.tagType == TypeAsciiNoNul {
values, err = vc.ReadAsciiNoNul()
log.PanicIf(err)
} else if vc.tagType == TypeShort {
values, err = vc.ReadShorts()
log.PanicIf(err)
} else if vc.tagType == TypeLong {
values, err = vc.ReadLongs()
log.PanicIf(err)
} else if vc.tagType == TypeRational {
values, err = vc.ReadRationals()
log.PanicIf(err)
} else if vc.tagType == TypeSignedLong {
values, err = vc.ReadSignedLongs()
log.PanicIf(err)
} else if vc.tagType == TypeSignedRational {
values, err = vc.ReadSignedRationals()
log.PanicIf(err)
} else if vc.tagType == TypeUndefined {
log.Panicf("will not parse undefined-type value")
// Never called.
return nil, nil
} else {
log.Panicf("value of type [%s] is unparseable", vc.tagType)
// Never called.
return nil, nil
}
return values, nil
}
// Undefined attempts to identify and decode supported undefined-type fields.
// This is the primary, preferred interface to reading undefined values.
func (vc *ValueContext) Undefined() (value interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
value, err = UndefinedValue(vc.ifdPath, vc.tagId, vc, vc.byteOrder)
if err != nil {
if err == ErrUnhandledUnknownTypedTag {
return nil, err
}
log.Panic(err)
}
return value, nil
}
func init() {
parser = &Parser{}
}

View file

@ -1,21 +0,0 @@
language: go
go:
- master
- stable
- "1.14"
- "1.13"
- "1.12"
env:
- GO111MODULE=on
install:
- go get -t ./...
script:
# v1
- go test -v .
# v2
- cd v2
- go test -v ./... -coverprofile=coverage.txt -covermode=atomic
- cd ..
after_success:
- cd v2
- curl -s https://codecov.io/bash | bash

View file

@ -1,5 +1,5 @@
[![Build Status](https://travis-ci.org/dsoprea/go-jpeg-image-structure.svg?branch=master)](https://travis-ci.org/dsoprea/go-jpeg-image-structure) [![Build Status](https://travis-ci.org/dsoprea/go-jpeg-image-structure/v2.svg?branch=master)](https://travis-ci.org/dsoprea/go-jpeg-image-structure/v2)
[![codecov](https://codecov.io/gh/dsoprea/go-jpeg-image-structure/branch/master/graph/badge.svg?token=Twxyx7kpAa)](https://codecov.io/gh/dsoprea/go-jpeg-image-structure) [![codecov](https://codecov.io/gh/dsoprea/go-jpeg-image-structure/branch/master/graph/badge.svg)](https://codecov.io/gh/dsoprea/go-jpeg-image-structure)
[![Go Report Card](https://goreportcard.com/badge/github.com/dsoprea/go-jpeg-image-structure/v2)](https://goreportcard.com/report/github.com/dsoprea/go-jpeg-image-structure/v2) [![Go Report Card](https://goreportcard.com/badge/github.com/dsoprea/go-jpeg-image-structure/v2)](https://goreportcard.com/report/github.com/dsoprea/go-jpeg-image-structure/v2)
[![GoDoc](https://godoc.org/github.com/dsoprea/go-jpeg-image-structure/v2?status.svg)](https://godoc.org/github.com/dsoprea/go-jpeg-image-structure/v2) [![GoDoc](https://godoc.org/github.com/dsoprea/go-jpeg-image-structure/v2?status.svg)](https://godoc.org/github.com/dsoprea/go-jpeg-image-structure/v2)

View file

@ -3,11 +3,14 @@ package jpegstructure
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"image"
"io" "io"
"os" "os"
"image/jpeg"
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-utility/image" "github.com/dsoprea/go-utility/v2/image"
) )
// JpegMediaParser is a `riimage.MediaParser` that knows how to parse JPEG // JpegMediaParser is a `riimage.MediaParser` that knows how to parse JPEG
@ -122,6 +125,14 @@ func (jmp *JpegMediaParser) LooksLikeFormat(data []byte) bool {
return true return true
} }
// GetImage returns an image.Image-compatible struct.
func (jmp *JpegMediaParser) GetImage(r io.Reader) (img image.Image, err error) {
img, err = jpeg.Decode(r)
log.PanicIf(err)
return img, nil
}
var ( var (
// Enforce interface conformance. // Enforce interface conformance.
_ riimage.MediaParser = new(JpegMediaParser) _ riimage.MediaParser = new(JpegMediaParser)

View file

@ -8,11 +8,12 @@ import (
"crypto/sha1" "crypto/sha1"
"encoding/hex" "encoding/hex"
"github.com/dsoprea/go-exif/v2" "github.com/dsoprea/go-exif/v3"
"github.com/dsoprea/go-exif/v3/common"
"github.com/dsoprea/go-iptc" "github.com/dsoprea/go-iptc"
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-photoshop-info-format" "github.com/dsoprea/go-photoshop-info-format"
"github.com/dsoprea/go-utility/image" "github.com/dsoprea/go-utility/v2/image"
) )
const ( const (
@ -125,7 +126,9 @@ func (s *Segment) Exif() (rootIfd *exif.Ifd, data []byte, err error) {
jpegLogger.Debugf(nil, "Attempting to parse (%d) byte EXIF blob (Exif).", len(rawExif)) jpegLogger.Debugf(nil, "Attempting to parse (%d) byte EXIF blob (Exif).", len(rawExif))
im := exif.NewIfdMappingWithStandard() im, err := exifcommon.NewIfdMappingWithStandard()
log.PanicIf(err)
ti := exif.NewTagIndex() ti := exif.NewTagIndex()
_, index, err := exif.Collect(im, ti, rawExif) _, index, err := exif.Collect(im, ti, rawExif)
@ -150,7 +153,7 @@ func (s *Segment) FlatExif() (exifTags []exif.ExifTag, err error) {
jpegLogger.Debugf(nil, "Attempting to parse (%d) byte EXIF blob (FlatExif).", len(rawExif)) jpegLogger.Debugf(nil, "Attempting to parse (%d) byte EXIF blob (FlatExif).", len(rawExif))
exifTags, err = exif.GetFlatExifData(rawExif) exifTags, _, err = exif.GetFlatExifData(rawExif, nil)
log.PanicIf(err) log.PanicIf(err)
return exifTags, nil return exifTags, nil

View file

@ -8,7 +8,8 @@ import (
"crypto/sha1" "crypto/sha1"
"encoding/binary" "encoding/binary"
"github.com/dsoprea/go-exif/v2" "github.com/dsoprea/go-exif/v3"
"github.com/dsoprea/go-exif/v3/common"
"github.com/dsoprea/go-iptc" "github.com/dsoprea/go-iptc"
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
) )
@ -241,11 +242,31 @@ func (sl *SegmentList) ConstructExifBuilder() (rootIb *exif.IfdBuilder, err erro
}() }()
rootIfd, _, err := sl.Exif() rootIfd, _, err := sl.Exif()
log.PanicIf(err) if log.Is(err, exif.ErrNoExif) == true {
// No EXIF. Just create a boilerplate builder.
ib := exif.NewIfdBuilderFromExistingChain(rootIfd) im := exifcommon.NewIfdMapping()
return ib, nil err := exifcommon.LoadStandardIfds(im)
log.PanicIf(err)
ti := exif.NewTagIndex()
rootIb :=
exif.NewIfdBuilder(
im,
ti,
exifcommon.IfdStandardIfdIdentity,
exifcommon.EncodeDefaultByteOrder)
return rootIb, nil
} else if err != nil {
log.Panic(err)
}
rootIb = exif.NewIfdBuilderFromExistingChain(rootIfd)
return rootIb, nil
} }
// DumpExif returns an unstructured list of tags (useful when just reviewing). // DumpExif returns an unstructured list of tags (useful when just reviewing).

View file

@ -1,21 +0,0 @@
language: go
go:
- master
- stable
- "1.14"
- "1.13"
- "1.12"
env:
- GO111MODULE=on
install:
- go get -t ./...
script:
# v1
- go test -v .
# v2
- cd v2
- go test -v .
- cd ..
after_success:
- cd v2
- curl -s https://codecov.io/bash | bash

View file

@ -1,8 +0,0 @@
[![Build Status](https://travis-ci.org/dsoprea/go-png-image-structure.svg?branch=master)](https://travis-ci.org/dsoprea/go-png-image-structure)
[![codecov](https://codecov.io/gh/dsoprea/go-png-image-structure/branch/master/graph/badge.svg)](https://codecov.io/gh/dsoprea/go-png-image-structure)
[![Go Report Card](https://goreportcard.com/badge/github.com/dsoprea/go-png-image-structure/v2)](https://goreportcard.com/report/github.com/dsoprea/go-png-image-structure/v2)
[![GoDoc](https://godoc.org/github.com/dsoprea/go-png-image-structure/v2?status.svg)](https://godoc.org/github.com/dsoprea/go-png-image-structure/v2)
## Overview
Parse raw PNG data into individual chunks. Parse/modify EXIF data and write an updated image.

View file

@ -1,89 +0,0 @@
package pngstructure
import (
"fmt"
"bytes"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
type ChunkDecoder struct {
}
func NewChunkDecoder() *ChunkDecoder {
return new(ChunkDecoder)
}
func (cd *ChunkDecoder) Decode(c *Chunk) (decoded interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
switch c.Type {
case "IHDR":
ihdr, err := cd.decodeIHDR(c)
log.PanicIf(err)
return ihdr, nil
}
// We don't decode this particular type.
return nil, nil
}
type ChunkIHDR struct {
Width uint32
Height uint32
BitDepth uint8
ColorType uint8
CompressionMethod uint8
FilterMethod uint8
InterlaceMethod uint8
}
func (ihdr *ChunkIHDR) String() string {
return fmt.Sprintf("IHDR<WIDTH=(%d) HEIGHT=(%d) DEPTH=(%d) COLOR-TYPE=(%d) COMP-METHOD=(%d) FILTER-METHOD=(%d) INTRLC-METHOD=(%d)>", ihdr.Width, ihdr.Height, ihdr.BitDepth, ihdr.ColorType, ihdr.CompressionMethod, ihdr.FilterMethod, ihdr.InterlaceMethod)
}
func (cd *ChunkDecoder) decodeIHDR(c *Chunk) (ihdr *ChunkIHDR, err error) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
b := bytes.NewBuffer(c.Data)
ihdr = new(ChunkIHDR)
err = binary.Read(b, binary.BigEndian, &ihdr.Width)
log.PanicIf(err)
err = binary.Read(b, binary.BigEndian, &ihdr.Height)
log.PanicIf(err)
err = binary.Read(b, binary.BigEndian, &ihdr.BitDepth)
log.PanicIf(err)
err = binary.Read(b, binary.BigEndian, &ihdr.ColorType)
log.PanicIf(err)
err = binary.Read(b, binary.BigEndian, &ihdr.CompressionMethod)
log.PanicIf(err)
err = binary.Read(b, binary.BigEndian, &ihdr.FilterMethod)
log.PanicIf(err)
err = binary.Read(b, binary.BigEndian, &ihdr.InterlaceMethod)
log.PanicIf(err)
return ihdr, nil
}

View file

@ -1,106 +0,0 @@
package pngstructure
import (
"bufio"
"bytes"
"io"
"os"
"github.com/dsoprea/go-logging"
"github.com/dsoprea/go-utility/image"
)
// PngMediaParser knows how to parse a PNG stream.
type PngMediaParser struct {
}
// NewPngMediaParser returns a new `PngMediaParser` struct.
func NewPngMediaParser() *PngMediaParser {
// TODO(dustin): Add test
return new(PngMediaParser)
}
// Parse parses a PNG stream given a `io.ReadSeeker`.
func (pmp *PngMediaParser) Parse(rs io.ReadSeeker, size int) (mc riimage.MediaContext, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): Add test
ps := NewPngSplitter()
err = ps.readHeader(rs)
log.PanicIf(err)
s := bufio.NewScanner(rs)
// Since each segment can be any size, our buffer must be allowed to grow
// as large as the file.
buffer := []byte{}
s.Buffer(buffer, size)
s.Split(ps.Split)
for s.Scan() != false {
}
log.PanicIf(s.Err())
return ps.Chunks(), nil
}
// ParseFile parses a PNG stream given a file-path.
func (pmp *PngMediaParser) ParseFile(filepath string) (mc riimage.MediaContext, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
f, err := os.Open(filepath)
log.PanicIf(err)
defer f.Close()
stat, err := f.Stat()
log.PanicIf(err)
size := stat.Size()
chunks, err := pmp.Parse(f, int(size))
log.PanicIf(err)
return chunks, nil
}
// ParseBytes parses a PNG stream given a byte-slice.
func (pmp *PngMediaParser) ParseBytes(data []byte) (mc riimage.MediaContext, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): Add test
br := bytes.NewReader(data)
chunks, err := pmp.Parse(br, len(data))
log.PanicIf(err)
return chunks, nil
}
// LooksLikeFormat returns a boolean indicating whether the stream looks like a
// PNG image.
func (pmp *PngMediaParser) LooksLikeFormat(data []byte) bool {
return bytes.Compare(data[:len(PngSignature)], PngSignature[:]) == 0
}
var (
// Enforce interface conformance.
_ riimage.MediaParser = new(PngMediaParser)
)

View file

@ -1,64 +0,0 @@
package pngstructure
import (
"os"
"path"
"github.com/dsoprea/go-logging"
)
var (
assetsPath = ""
)
func getModuleRootPath() string {
moduleRootPath := os.Getenv("PNG_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)
}
}
return currentPath
}
func getTestAssetsPath() string {
if assetsPath == "" {
moduleRootPath := getModuleRootPath()
assetsPath = path.Join(moduleRootPath, "assets")
}
return assetsPath
}
func getTestBasicImageFilepath() string {
assetsPath := getTestAssetsPath()
return path.Join(assetsPath, "libpng.png")
}
func getTestExifImageFilepath() string {
assetsPath := getTestAssetsPath()
return path.Join(assetsPath, "exif.png")
}

View file

@ -1,65 +0,0 @@
package pngstructure
import (
"fmt"
"bytes"
"github.com/dsoprea/go-logging"
)
func DumpBytes(data []byte) {
fmt.Printf("DUMP: ")
for _, x := range data {
fmt.Printf("%02x ", x)
}
fmt.Printf("\n")
}
func DumpBytesClause(data []byte) {
fmt.Printf("DUMP: ")
fmt.Printf("[]byte { ")
for i, x := range data {
fmt.Printf("0x%02x", x)
if i < len(data) - 1 {
fmt.Printf(", ")
}
}
fmt.Printf(" }\n")
}
func DumpBytesToString(data []byte) string {
b := new(bytes.Buffer)
for i, x := range data {
_, err := b.WriteString(fmt.Sprintf("%02x", x))
log.PanicIf(err)
if i < len(data) - 1 {
_, err := b.WriteRune(' ')
log.PanicIf(err)
}
}
return b.String()
}
func DumpBytesClauseToString(data []byte) string {
b := new(bytes.Buffer)
for i, x := range data {
_, err := b.WriteString(fmt.Sprintf("0x%02x", x))
log.PanicIf(err)
if i < len(data) - 1 {
_, err := b.WriteString(", ")
log.PanicIf(err)
}
}
return b.String()
}

View file

@ -0,0 +1,87 @@
package pngstructure
import (
"bytes"
"fmt"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
type ChunkDecoder struct {
}
func NewChunkDecoder() *ChunkDecoder {
return new(ChunkDecoder)
}
func (cd *ChunkDecoder) Decode(c *Chunk) (decoded interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
switch c.Type {
case "IHDR":
ihdr, err := cd.decodeIHDR(c)
log.PanicIf(err)
return ihdr, nil
}
// We don't decode this particular type.
return nil, nil
}
type ChunkIHDR struct {
Width uint32
Height uint32
BitDepth uint8
ColorType uint8
CompressionMethod uint8
FilterMethod uint8
InterlaceMethod uint8
}
func (ihdr *ChunkIHDR) String() string {
return fmt.Sprintf("IHDR<WIDTH=(%d) HEIGHT=(%d) DEPTH=(%d) COLOR-TYPE=(%d) COMP-METHOD=(%d) FILTER-METHOD=(%d) INTRLC-METHOD=(%d)>", ihdr.Width, ihdr.Height, ihdr.BitDepth, ihdr.ColorType, ihdr.CompressionMethod, ihdr.FilterMethod, ihdr.InterlaceMethod)
}
func (cd *ChunkDecoder) decodeIHDR(c *Chunk) (ihdr *ChunkIHDR, err error) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
b := bytes.NewBuffer(c.Data)
ihdr = new(ChunkIHDR)
err = binary.Read(b, binary.BigEndian, &ihdr.Width)
log.PanicIf(err)
err = binary.Read(b, binary.BigEndian, &ihdr.Height)
log.PanicIf(err)
err = binary.Read(b, binary.BigEndian, &ihdr.BitDepth)
log.PanicIf(err)
err = binary.Read(b, binary.BigEndian, &ihdr.ColorType)
log.PanicIf(err)
err = binary.Read(b, binary.BigEndian, &ihdr.CompressionMethod)
log.PanicIf(err)
err = binary.Read(b, binary.BigEndian, &ihdr.FilterMethod)
log.PanicIf(err)
err = binary.Read(b, binary.BigEndian, &ihdr.InterlaceMethod)
log.PanicIf(err)
return ihdr, nil
}

View file

@ -0,0 +1,118 @@
package pngstructure
import (
"bufio"
"bytes"
"image"
"io"
"os"
"image/png"
"github.com/dsoprea/go-logging"
"github.com/dsoprea/go-utility/v2/image"
)
// PngMediaParser knows how to parse a PNG stream.
type PngMediaParser struct {
}
// NewPngMediaParser returns a new `PngMediaParser` struct.
func NewPngMediaParser() *PngMediaParser {
// TODO(dustin): Add test
return new(PngMediaParser)
}
// Parse parses a PNG stream given a `io.ReadSeeker`.
func (pmp *PngMediaParser) Parse(rs io.ReadSeeker, size int) (mc riimage.MediaContext, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): Add test
ps := NewPngSplitter()
err = ps.readHeader(rs)
log.PanicIf(err)
s := bufio.NewScanner(rs)
// Since each segment can be any size, our buffer must be allowed to grow
// as large as the file.
buffer := []byte{}
s.Buffer(buffer, size)
s.Split(ps.Split)
for s.Scan() != false {
}
log.PanicIf(s.Err())
return ps.Chunks(), nil
}
// ParseFile parses a PNG stream given a file-path.
func (pmp *PngMediaParser) ParseFile(filepath string) (mc riimage.MediaContext, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
f, err := os.Open(filepath)
log.PanicIf(err)
defer f.Close()
stat, err := f.Stat()
log.PanicIf(err)
size := stat.Size()
chunks, err := pmp.Parse(f, int(size))
log.PanicIf(err)
return chunks, nil
}
// ParseBytes parses a PNG stream given a byte-slice.
func (pmp *PngMediaParser) ParseBytes(data []byte) (mc riimage.MediaContext, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): Add test
br := bytes.NewReader(data)
chunks, err := pmp.Parse(br, len(data))
log.PanicIf(err)
return chunks, nil
}
// LooksLikeFormat returns a boolean indicating whether the stream looks like a
// PNG image.
func (pmp *PngMediaParser) LooksLikeFormat(data []byte) bool {
return bytes.Compare(data[:len(PngSignature)], PngSignature[:]) == 0
}
// GetImage returns an image.Image-compatible struct.
func (pmp *PngMediaParser) GetImage(r io.Reader) (img image.Image, err error) {
img, err = png.Decode(r)
log.PanicIf(err)
return img, nil
}
var (
// Enforce interface conformance.
_ riimage.MediaParser = new(PngMediaParser)
)

View file

@ -9,9 +9,10 @@ import (
"encoding/binary" "encoding/binary"
"hash/crc32" "hash/crc32"
"github.com/dsoprea/go-exif/v2" "github.com/dsoprea/go-exif/v3"
"github.com/dsoprea/go-exif/v3/common"
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
"github.com/dsoprea/go-utility/image" "github.com/dsoprea/go-utility/v2/image"
) )
var ( var (
@ -22,7 +23,6 @@ var (
var ( var (
ErrNotPng = errors.New("not png data") ErrNotPng = errors.New("not png data")
ErrNoExif = errors.New("file does not have EXIF")
ErrCrcFailure = errors.New("crc failure") ErrCrcFailure = errors.New("crc failure")
) )
@ -111,7 +111,7 @@ func (cs *ChunkSlice) FindExif() (chunk *Chunk, err error) {
return chunks[0], nil return chunks[0], nil
} }
log.Panic(ErrNoExif) log.Panic(exif.ErrNoExif)
// Never called. // Never called.
return nil, nil return nil, nil
@ -128,7 +128,9 @@ func (cs *ChunkSlice) Exif() (rootIfd *exif.Ifd, data []byte, err error) {
chunk, err := cs.FindExif() chunk, err := cs.FindExif()
log.PanicIf(err) log.PanicIf(err)
im := exif.NewIfdMappingWithStandard() im, err := exifcommon.NewIfdMappingWithStandard()
log.PanicIf(err)
ti := exif.NewTagIndex() ti := exif.NewTagIndex()
// TODO(dustin): Refactor and support `exif.GetExifData()`. // TODO(dustin): Refactor and support `exif.GetExifData()`.
@ -180,7 +182,7 @@ func (cs *ChunkSlice) SetExif(ib *exif.IfdBuilder) (err error) {
exifChunk.Data = exifData exifChunk.Data = exifData
exifChunk.Length = uint32(len(exifData)) exifChunk.Length = uint32(len(exifData))
} else { } else {
if log.Is(err, ErrNoExif) != true { if log.Is(err, exif.ErrNoExif) != true {
log.Panic(err) log.Panic(err)
} }

View file

@ -0,0 +1,64 @@
package pngstructure
import (
"os"
"path"
"github.com/dsoprea/go-logging"
)
var (
assetsPath = ""
)
func getModuleRootPath() string {
moduleRootPath := os.Getenv("PNG_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)
}
}
return currentPath
}
func getTestAssetsPath() string {
if assetsPath == "" {
moduleRootPath := getModuleRootPath()
assetsPath = path.Join(moduleRootPath, "assets")
}
return assetsPath
}
func getTestBasicImageFilepath() string {
assetsPath := getTestAssetsPath()
return path.Join(assetsPath, "libpng.png")
}
func getTestExifImageFilepath() string {
assetsPath := getTestAssetsPath()
return path.Join(assetsPath, "exif.png")
}

View file

@ -1,14 +1,12 @@
package exifcommon package pngstructure
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"time"
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
) )
// DumpBytes prints a list of hex-encoded bytes.
func DumpBytes(data []byte) { func DumpBytes(data []byte) {
fmt.Printf("DUMP: ") fmt.Printf("DUMP: ")
for _, x := range data { for _, x := range data {
@ -18,8 +16,6 @@ func DumpBytes(data []byte) {
fmt.Printf("\n") fmt.Printf("\n")
} }
// DumpBytesClause prints a list like DumpBytes(), but encapsulated in
// "[]byte { ... }".
func DumpBytesClause(data []byte) { func DumpBytesClause(data []byte) {
fmt.Printf("DUMP: ") fmt.Printf("DUMP: ")
@ -36,7 +32,6 @@ func DumpBytesClause(data []byte) {
fmt.Printf(" }\n") fmt.Printf(" }\n")
} }
// DumpBytesToString returns a stringified list of hex-encoded bytes.
func DumpBytesToString(data []byte) string { func DumpBytesToString(data []byte) string {
b := new(bytes.Buffer) b := new(bytes.Buffer)
@ -53,7 +48,6 @@ func DumpBytesToString(data []byte) string {
return b.String() return b.String()
} }
// DumpBytesClauseToString returns a comma-separated list of hex-encoded bytes.
func DumpBytesClauseToString(data []byte) string { func DumpBytesClauseToString(data []byte) string {
b := new(bytes.Buffer) b := new(bytes.Buffer)
@ -69,11 +63,3 @@ func DumpBytesClauseToString(data []byte) string {
return b.String() 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())
}

View file

@ -0,0 +1,64 @@
[![GoDoc](https://godoc.org/github.com/dsoprea/go-utility/filesystem?status.svg)](https://godoc.org/github.com/dsoprea/go-utility/filesystem)
[![Build Status](https://travis-ci.org/dsoprea/go-utility.svg?branch=master)](https://travis-ci.org/dsoprea/go-utility)
[![Coverage Status](https://coveralls.io/repos/github/dsoprea/go-utility/badge.svg?branch=master)](https://coveralls.io/github/dsoprea/go-utility?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/dsoprea/go-utility)](https://goreportcard.com/report/github.com/dsoprea/go-utility)
# bounceback
An `io.ReadSeeker` and `io.WriteSeeker` that returns to the right place before
reading or writing. Useful when the same file resource is being reused for reads
or writes throughout that file.
# list_files
A recursive path walker that supports filters.
# seekable_buffer
A memory structure that satisfies `io.ReadWriteSeeker`.
# copy_bytes_between_positions
Given an `io.ReadWriteSeeker`, copy N bytes from one position to an earlier
position.
# read_counter, write_counter
Wrap `io.Reader` and `io.Writer` structs in order to report how many bytes were
transferred.
# readseekwritecloser
Provides the ReadWriteSeekCloser interface that combines a RWS and a Closer.
Also provides a no-op wrapper to augment a plain RWS with a closer.
# boundedreadwriteseek
Wraps a ReadWriteSeeker such that no seeks can be at an offset less than a
specific-offset.
# calculateseek
Provides a reusable function with which to calculate seek offsets.
# progress_wrapper
Provides `io.Reader` and `io.Writer` wrappers that also trigger callbacks after
each call. The reader wrapper also invokes the callback upon EOF.
# does_exist
Check whether a file/directory exists using a file-path.
# graceful_copy
Do a copy but correctly handle short-writes and reads that might return a non-
zero read count *and* EOF.
# readseeker_to_readerat
A wrapper that allows an `io.ReadSeeker` to be used as a `io.ReaderAt`.
# simplefileinfo
An implementation of `os.FileInfo` to support testing.

View file

@ -0,0 +1,273 @@
package rifs
import (
"fmt"
"io"
"github.com/dsoprea/go-logging"
)
// BouncebackStats describes operation counts.
type BouncebackStats struct {
reads int
writes int
seeks int
syncs int
}
func (bbs BouncebackStats) String() string {
return fmt.Sprintf(
"BouncebackStats<READS=(%d) WRITES=(%d) SEEKS=(%d) SYNCS=(%d)>",
bbs.reads, bbs.writes, bbs.seeks, bbs.syncs)
}
type bouncebackBase struct {
currentPosition int64
stats BouncebackStats
}
// Position returns the position that we're supposed to be at.
func (bb *bouncebackBase) Position() int64 {
// TODO(dustin): Add test
return bb.currentPosition
}
// StatsReads returns the number of reads that have been attempted.
func (bb *bouncebackBase) StatsReads() int {
// TODO(dustin): Add test
return bb.stats.reads
}
// StatsWrites returns the number of write operations.
func (bb *bouncebackBase) StatsWrites() int {
// TODO(dustin): Add test
return bb.stats.writes
}
// StatsSeeks returns the number of seeks.
func (bb *bouncebackBase) StatsSeeks() int {
// TODO(dustin): Add test
return bb.stats.seeks
}
// StatsSyncs returns the number of corrective seeks ("bounce-backs").
func (bb *bouncebackBase) StatsSyncs() int {
// TODO(dustin): Add test
return bb.stats.syncs
}
// Seek does a seek to an arbitrary place in the `io.ReadSeeker`.
func (bb *bouncebackBase) seek(s io.Seeker, offset int64, whence int) (newPosition int64, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// If the seek is relative, make sure we're where we're supposed to be *first*.
if whence != io.SeekStart {
err = bb.checkPosition(s)
log.PanicIf(err)
}
bb.stats.seeks++
newPosition, err = s.Seek(offset, whence)
log.PanicIf(err)
// Update our internal tracking.
bb.currentPosition = newPosition
return newPosition, nil
}
func (bb *bouncebackBase) checkPosition(s io.Seeker) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// Make sure we're where we're supposed to be.
// This should have no overhead, and enables us to collect stats.
realCurrentPosition, err := s.Seek(0, io.SeekCurrent)
log.PanicIf(err)
if realCurrentPosition != bb.currentPosition {
bb.stats.syncs++
_, err = s.Seek(bb.currentPosition, io.SeekStart)
log.PanicIf(err)
}
return nil
}
// BouncebackReader wraps a ReadSeeker, keeps track of our position, and
// seeks back to it before writing. This allows an underlying ReadWriteSeeker
// with an unstable position can still be used for a prolonged series of writes.
type BouncebackReader struct {
rs io.ReadSeeker
bouncebackBase
}
// NewBouncebackReader returns a `*BouncebackReader` struct.
func NewBouncebackReader(rs io.ReadSeeker) (br *BouncebackReader, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
initialPosition, err := rs.Seek(0, io.SeekCurrent)
log.PanicIf(err)
bb := bouncebackBase{
currentPosition: initialPosition,
}
br = &BouncebackReader{
rs: rs,
bouncebackBase: bb,
}
return br, nil
}
// Seek does a seek to an arbitrary place in the `io.ReadSeeker`.
func (br *BouncebackReader) Seek(offset int64, whence int) (newPosition int64, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
newPosition, err = br.bouncebackBase.seek(br.rs, offset, whence)
log.PanicIf(err)
return newPosition, nil
}
// Seek does a standard read.
func (br *BouncebackReader) Read(p []byte) (n int, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
br.bouncebackBase.stats.reads++
err = br.bouncebackBase.checkPosition(br.rs)
log.PanicIf(err)
// Do read.
n, err = br.rs.Read(p)
if err != nil {
if err == io.EOF {
return 0, io.EOF
}
log.Panic(err)
}
// Update our internal tracking.
br.bouncebackBase.currentPosition += int64(n)
return n, nil
}
// BouncebackWriter wraps a WriteSeeker, keeps track of our position, and
// seeks back to it before writing. This allows an underlying ReadWriteSeeker
// with an unstable position can still be used for a prolonged series of writes.
type BouncebackWriter struct {
ws io.WriteSeeker
bouncebackBase
}
// NewBouncebackWriter returns a new `BouncebackWriter` struct.
func NewBouncebackWriter(ws io.WriteSeeker) (bw *BouncebackWriter, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
initialPosition, err := ws.Seek(0, io.SeekCurrent)
log.PanicIf(err)
bb := bouncebackBase{
currentPosition: initialPosition,
}
bw = &BouncebackWriter{
ws: ws,
bouncebackBase: bb,
}
return bw, nil
}
// Seek puts us at a specific position in the internal writer for the next
// write/seek.
func (bw *BouncebackWriter) Seek(offset int64, whence int) (newPosition int64, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
newPosition, err = bw.bouncebackBase.seek(bw.ws, offset, whence)
log.PanicIf(err)
return newPosition, nil
}
// Write performs a write against the internal `WriteSeeker` starting at the
// position that we're supposed to be at.
func (bw *BouncebackWriter) Write(p []byte) (n int, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
bw.bouncebackBase.stats.writes++
// Make sure we're where we're supposed to be.
realCurrentPosition, err := bw.ws.Seek(0, io.SeekCurrent)
log.PanicIf(err)
if realCurrentPosition != bw.bouncebackBase.currentPosition {
bw.bouncebackBase.stats.seeks++
_, err = bw.ws.Seek(bw.bouncebackBase.currentPosition, io.SeekStart)
log.PanicIf(err)
}
// Do write.
n, err = bw.ws.Write(p)
log.PanicIf(err)
// Update our internal tracking.
bw.bouncebackBase.currentPosition += int64(n)
return n, nil
}

View file

@ -0,0 +1,95 @@
package rifs
import (
"io"
"github.com/dsoprea/go-logging"
)
// BoundedReadWriteSeekCloser wraps a RWS that is also a closer with boundaries.
// This proxies the RWS methods to the inner BRWS inside.
type BoundedReadWriteSeekCloser struct {
io.Closer
*BoundedReadWriteSeeker
}
// NewBoundedReadWriteSeekCloser returns a new BoundedReadWriteSeekCloser.
func NewBoundedReadWriteSeekCloser(rwsc ReadWriteSeekCloser, minimumOffset int64, staticFileSize int64) (brwsc *BoundedReadWriteSeekCloser, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
bs, err := NewBoundedReadWriteSeeker(rwsc, minimumOffset, staticFileSize)
log.PanicIf(err)
brwsc = &BoundedReadWriteSeekCloser{
Closer: rwsc,
BoundedReadWriteSeeker: bs,
}
return brwsc, nil
}
// Seek forwards calls to the inner RWS.
func (rwsc *BoundedReadWriteSeekCloser) Seek(offset int64, whence int) (newOffset int64, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
newOffset, err = rwsc.BoundedReadWriteSeeker.Seek(offset, whence)
log.PanicIf(err)
return newOffset, nil
}
// Read forwards calls to the inner RWS.
func (rwsc *BoundedReadWriteSeekCloser) Read(buffer []byte) (readCount int, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
readCount, err = rwsc.BoundedReadWriteSeeker.Read(buffer)
if err != nil {
if err == io.EOF {
return 0, err
}
log.Panic(err)
}
return readCount, nil
}
// Write forwards calls to the inner RWS.
func (rwsc *BoundedReadWriteSeekCloser) Write(buffer []byte) (writtenCount int, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
writtenCount, err = rwsc.BoundedReadWriteSeeker.Write(buffer)
log.PanicIf(err)
return writtenCount, nil
}
// Close forwards calls to the inner RWS.
func (rwsc *BoundedReadWriteSeekCloser) Close() (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
err = rwsc.Closer.Close()
log.PanicIf(err)
return nil
}

View file

@ -0,0 +1,156 @@
package rifs
import (
"errors"
"io"
"os"
"github.com/dsoprea/go-logging"
)
var (
// ErrSeekBeyondBound is returned when a seek is requested beyond the
// statically-given file-size. No writes or seeks beyond boundaries are
// supported with a statically-given file size.
ErrSeekBeyondBound = errors.New("seek beyond boundary")
)
// BoundedReadWriteSeeker is a thin filter that ensures that no seeks can be done
// to offsets smaller than the one we were given. This supports libraries that
// might be expecting to read from the front of the stream being used on data
// that is in the middle of a stream instead.
type BoundedReadWriteSeeker struct {
io.ReadWriteSeeker
currentOffset int64
minimumOffset int64
staticFileSize int64
}
// NewBoundedReadWriteSeeker returns a new BoundedReadWriteSeeker instance.
func NewBoundedReadWriteSeeker(rws io.ReadWriteSeeker, minimumOffset int64, staticFileSize int64) (brws *BoundedReadWriteSeeker, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if minimumOffset < 0 {
log.Panicf("BoundedReadWriteSeeker minimum offset must be zero or larger: (%d)", minimumOffset)
}
// We'll always started at a relative offset of zero.
_, err = rws.Seek(minimumOffset, os.SEEK_SET)
log.PanicIf(err)
brws = &BoundedReadWriteSeeker{
ReadWriteSeeker: rws,
currentOffset: 0,
minimumOffset: minimumOffset,
staticFileSize: staticFileSize,
}
return brws, nil
}
// Seek moves the offset to the given offset. Prevents offset from ever being
// moved left of `brws.minimumOffset`.
func (brws *BoundedReadWriteSeeker) Seek(offset int64, whence int) (updatedOffset int64, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
fileSize := brws.staticFileSize
// If we weren't given a static file-size, look it up whenever it is needed.
if whence == os.SEEK_END && fileSize == 0 {
realFileSizeRaw, err := brws.ReadWriteSeeker.Seek(0, os.SEEK_END)
log.PanicIf(err)
fileSize = realFileSizeRaw - brws.minimumOffset
}
updatedOffset, err = CalculateSeek(brws.currentOffset, offset, whence, fileSize)
log.PanicIf(err)
if brws.staticFileSize != 0 && updatedOffset > brws.staticFileSize {
//updatedOffset = int64(brws.staticFileSize)
// NOTE(dustin): Presumably, this will only be disruptive to writes that are beyond the boundaries, which, if we're being used at all, should already account for the boundary and prevent this error from ever happening. So, time will tell how disruptive this is.
return 0, ErrSeekBeyondBound
}
if updatedOffset != brws.currentOffset {
updatedRealOffset := updatedOffset + brws.minimumOffset
_, err = brws.ReadWriteSeeker.Seek(updatedRealOffset, os.SEEK_SET)
log.PanicIf(err)
brws.currentOffset = updatedOffset
}
return updatedOffset, nil
}
// Read forwards writes to the inner RWS.
func (brws *BoundedReadWriteSeeker) Read(buffer []byte) (readCount int, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if brws.staticFileSize != 0 {
availableCount := brws.staticFileSize - brws.currentOffset
if availableCount == 0 {
return 0, io.EOF
}
if int64(len(buffer)) > availableCount {
buffer = buffer[:availableCount]
}
}
readCount, err = brws.ReadWriteSeeker.Read(buffer)
brws.currentOffset += int64(readCount)
if err != nil {
if err == io.EOF {
return 0, err
}
log.Panic(err)
}
return readCount, nil
}
// Write forwards writes to the inner RWS.
func (brws *BoundedReadWriteSeeker) Write(buffer []byte) (writtenCount int, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if brws.staticFileSize != 0 {
log.Panicf("writes can not be performed if a static file-size was given")
}
writtenCount, err = brws.ReadWriteSeeker.Write(buffer)
brws.currentOffset += int64(writtenCount)
log.PanicIf(err)
return writtenCount, nil
}
// MinimumOffset returns the configured minimum-offset.
func (brws *BoundedReadWriteSeeker) MinimumOffset() int64 {
return brws.minimumOffset
}

View file

@ -0,0 +1,52 @@
package rifs
import (
"io"
"os"
"github.com/dsoprea/go-logging"
)
// SeekType is a convenience type to associate the different seek-types with
// printable descriptions.
type SeekType int
// String returns a descriptive string.
func (n SeekType) String() string {
if n == io.SeekCurrent {
return "SEEK-CURRENT"
} else if n == io.SeekEnd {
return "SEEK-END"
} else if n == io.SeekStart {
return "SEEK-START"
}
log.Panicf("unknown seek-type: (%d)", n)
return ""
}
// CalculateSeek calculates an offset in a file-stream given the parameters.
func CalculateSeek(currentOffset int64, delta int64, whence int, fileSize int64) (finalOffset int64, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
finalOffset = 0
}
}()
if whence == os.SEEK_SET {
finalOffset = delta
} else if whence == os.SEEK_CUR {
finalOffset = currentOffset + delta
} else if whence == os.SEEK_END {
finalOffset = fileSize + delta
} else {
log.Panicf("whence not valid: (%d)", whence)
}
if finalOffset < 0 {
finalOffset = 0
}
return finalOffset, nil
}

View file

@ -0,0 +1,15 @@
package rifs
import (
"os"
"path"
)
var (
appPath string
)
func init() {
goPath := os.Getenv("GOPATH")
appPath = path.Join(goPath, "src", "github.com", "dsoprea", "go-utility", "filesystem")
}

View file

@ -0,0 +1,40 @@
package rifs
import (
"io"
"os"
"github.com/dsoprea/go-logging"
)
// CopyBytesBetweenPositions will copy bytes from one position in the given RWS
// to an earlier position in the same RWS.
func CopyBytesBetweenPositions(rws io.ReadWriteSeeker, fromPosition, toPosition int64, count int) (n int, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if fromPosition <= toPosition {
log.Panicf("from position (%d) must be larger than to position (%d)", fromPosition, toPosition)
}
br, err := NewBouncebackReader(rws)
log.PanicIf(err)
_, err = br.Seek(fromPosition, os.SEEK_SET)
log.PanicIf(err)
bw, err := NewBouncebackWriter(rws)
log.PanicIf(err)
_, err = bw.Seek(toPosition, os.SEEK_SET)
log.PanicIf(err)
written, err := io.CopyN(bw, br, int64(count))
log.PanicIf(err)
n = int(written)
return n, nil
}

View file

@ -0,0 +1,19 @@
package rifs
import (
"os"
)
// DoesExist returns true if we can open the given file/path without error. We
// can't simply use `os.IsNotExist()` because we'll get a different error when
// the parent directory doesn't exist, and really the only important thing is if
// it exists *and* it's readable.
func DoesExist(filepath string) bool {
f, err := os.Open(filepath)
if err != nil {
return false
}
f.Close()
return true
}

View file

@ -0,0 +1,54 @@
package rifs
import (
"fmt"
"io"
)
const (
defaultCopyBufferSize = 1024 * 1024
)
// GracefulCopy willcopy while enduring lesser normal issues.
//
// - We'll ignore EOF if the read byte-count is more than zero. Only an EOF when
// zero bytes were read will terminate the loop.
//
// - Ignore short-writes. If less bytes were written than the bytes that were
// given, we'll keep trying until done.
func GracefulCopy(w io.Writer, r io.Reader, buffer []byte) (copyCount int, err error) {
if buffer == nil {
buffer = make([]byte, defaultCopyBufferSize)
}
for {
readCount, err := r.Read(buffer)
if err != nil {
if err != io.EOF {
err = fmt.Errorf("read error: %s", err.Error())
return 0, err
}
// Only break on EOF if no bytes were actually read.
if readCount == 0 {
break
}
}
writeBuffer := buffer[:readCount]
for len(writeBuffer) > 0 {
writtenCount, err := w.Write(writeBuffer)
if err != nil {
err = fmt.Errorf("write error: %s", err.Error())
return 0, err
}
writeBuffer = writeBuffer[writtenCount:]
}
copyCount += readCount
}
return copyCount, nil
}

View file

@ -0,0 +1,143 @@
package rifs
import (
"io"
"os"
"path"
"github.com/dsoprea/go-logging"
)
// FileListFilterPredicate is the callback predicate used for filtering.
type FileListFilterPredicate func(parent string, child os.FileInfo) (hit bool, err error)
// VisitedFile is one visited file.
type VisitedFile struct {
Filepath string
Info os.FileInfo
Index int
}
// ListFiles feeds a continuous list of files from a recursive folder scan. An
// optional predicate can be provided in order to filter. When done, the
// `filesC` channel is closed. If there's an error, the `errC` channel will
// receive it.
func ListFiles(rootPath string, cb FileListFilterPredicate) (filesC chan VisitedFile, count int, errC chan error) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
// Make sure the path exists.
f, err := os.Open(rootPath)
log.PanicIf(err)
f.Close()
// Do our thing.
filesC = make(chan VisitedFile, 100)
errC = make(chan error, 1)
index := 0
go func() {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
errC <- err
}
}()
queue := []string{rootPath}
for len(queue) > 0 {
// Pop the next folder to process off the queue.
var thisPath string
thisPath, queue = queue[0], queue[1:]
// Skip path if a symlink.
fi, err := os.Lstat(thisPath)
log.PanicIf(err)
if (fi.Mode() & os.ModeSymlink) > 0 {
continue
}
// Read information.
folderF, err := os.Open(thisPath)
if err != nil {
errC <- log.Wrap(err)
return
}
// Iterate through children.
for {
children, err := folderF.Readdir(1000)
if err == io.EOF {
break
} else if err != nil {
errC <- log.Wrap(err)
return
}
for _, child := range children {
filepath := path.Join(thisPath, child.Name())
// Skip if a file symlink.
fi, err := os.Lstat(filepath)
log.PanicIf(err)
if (fi.Mode() & os.ModeSymlink) > 0 {
continue
}
// If a predicate was given, determine if this child will be
// left behind.
if cb != nil {
hit, err := cb(thisPath, child)
if err != nil {
errC <- log.Wrap(err)
return
}
if hit == false {
continue
}
}
index++
// Push file to channel.
vf := VisitedFile{
Filepath: filepath,
Info: child,
Index: index,
}
filesC <- vf
// If a folder, queue for later processing.
if child.IsDir() == true {
queue = append(queue, filepath)
}
}
}
folderF.Close()
}
close(filesC)
close(errC)
}()
return filesC, index, errC
}

View file

@ -0,0 +1,93 @@
package rifs
import (
"io"
"time"
"github.com/dsoprea/go-logging"
)
// ProgressFunc receives progress updates.
type ProgressFunc func(n int, duration time.Duration, isEof bool) error
// WriteProgressWrapper wraps a reader and calls a callback after each read with
// count and duration info.
type WriteProgressWrapper struct {
w io.Writer
progressCb ProgressFunc
}
// NewWriteProgressWrapper returns a new WPW instance.
func NewWriteProgressWrapper(w io.Writer, progressCb ProgressFunc) io.Writer {
return &WriteProgressWrapper{
w: w,
progressCb: progressCb,
}
}
// Write does a write and calls the callback.
func (wpw *WriteProgressWrapper) Write(buffer []byte) (n int, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
startAt := time.Now()
n, err = wpw.w.Write(buffer)
log.PanicIf(err)
duration := time.Since(startAt)
err = wpw.progressCb(n, duration, false)
log.PanicIf(err)
return n, nil
}
// ReadProgressWrapper wraps a reader and calls a callback after each read with
// count and duration info.
type ReadProgressWrapper struct {
r io.Reader
progressCb ProgressFunc
}
// NewReadProgressWrapper returns a new RPW instance.
func NewReadProgressWrapper(r io.Reader, progressCb ProgressFunc) io.Reader {
return &ReadProgressWrapper{
r: r,
progressCb: progressCb,
}
}
// Read reads data and calls the callback.
func (rpw *ReadProgressWrapper) Read(buffer []byte) (n int, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
startAt := time.Now()
n, err = rpw.r.Read(buffer)
duration := time.Since(startAt)
if err != nil {
if err == io.EOF {
errInner := rpw.progressCb(n, duration, true)
log.PanicIf(errInner)
return n, err
}
log.Panic(err)
}
err = rpw.progressCb(n, duration, false)
log.PanicIf(err)
return n, nil
}

View file

@ -0,0 +1,36 @@
package rifs
import (
"io"
)
// ReadCounter proxies read requests and maintains a counter of bytes read.
type ReadCounter struct {
r io.Reader
counter int
}
// NewReadCounter returns a new `ReadCounter` struct wrapping a `Reader`.
func NewReadCounter(r io.Reader) *ReadCounter {
return &ReadCounter{
r: r,
}
}
// Count returns the total number of bytes read.
func (rc *ReadCounter) Count() int {
return rc.counter
}
// Reset resets the counter to zero.
func (rc *ReadCounter) Reset() {
rc.counter = 0
}
// Read forwards a read to the underlying `Reader` while bumping the counter.
func (rc *ReadCounter) Read(b []byte) (n int, err error) {
n, err = rc.r.Read(b)
rc.counter += n
return n, err
}

Some files were not shown because too many files have changed in this diff Show more