├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── linting.yml │ └── test.yml ├── .pre-commit-config.yaml ├── .pylintrc ├── CONTRIBUTING.rst ├── ChangeLog.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── exifread ├── __init__.py ├── __main__.py ├── cli.py ├── core │ ├── __init__.py │ ├── exceptions.py │ ├── exif_header.py │ ├── find_exif.py │ ├── heic.py │ ├── ifd_tag.py │ ├── jpeg.py │ ├── jxl.py │ ├── utils.py │ └── xmp.py ├── exif_log.py ├── py.typed ├── serialize.py ├── tags │ ├── __init__.py │ ├── exif.py │ ├── fields.py │ ├── makernote │ │ ├── __init__.py │ │ ├── apple.py │ │ ├── canon.py │ │ ├── casio.py │ │ ├── dji.py │ │ ├── fujifilm.py │ │ ├── nikon.py │ │ ├── olympus.py │ │ └── sony.py │ └── str_utils.py └── utils.py ├── pyproject.toml └── tests ├── __init__.py ├── resources ├── README.rst ├── avif │ └── mountains.avif ├── dump.txt ├── heic │ ├── heic_hdlr_box.jpg │ ├── mobile │ │ ├── HMD_Nokia_8.3_5G.heif │ │ ├── HMD_Nokia_8.3_5G_hdr.heif │ │ └── iphone_13_pro_max.heic │ ├── samplefilehub.heif │ └── spring_1440x960.heic ├── jpg │ ├── Canon_40D.jpg │ ├── Canon_40D_photoshop_import.jpg │ ├── Canon_DIGITAL_IXUS_400.jpg │ ├── Canon_PowerShot_S40.jpg │ ├── Fujifilm_FinePix6900ZOOM.jpg │ ├── Fujifilm_FinePix_E500.jpg │ ├── Kodak_CX7530.jpg │ ├── Konica_Minolta_DiMAGE_Z3.jpg │ ├── Nikon_COOLPIX_P1.jpg │ ├── Nikon_D70.jpg │ ├── Olympus_C8080WZ.jpg │ ├── PaintTool_sample.jpg │ ├── Panasonic_DMC-FZ30.jpg │ ├── Pentax_K10D.jpg │ ├── README.txt │ ├── Reconyx_HC500_Hyperfire.jpg │ ├── Ricoh_Caplio_RR330.jpg │ ├── Samsung_Digimax_i50_MP3.jpg │ ├── Sony_DSLR-A200.jpg │ ├── Sony_HDR-HC3.jpg │ ├── Sony_alpha_a58.JPG │ ├── WWL_Polaroid_ION230.jpg │ ├── corrupted.jpg │ ├── exif-org │ │ ├── README.txt │ │ ├── canon-ixus.jpg │ │ ├── fujifilm-dx10.jpg │ │ ├── fujifilm-finepix40i.jpg │ │ ├── fujifilm-mx1700.jpg │ │ ├── kodak-dc210.jpg │ │ ├── kodak-dc240.jpg │ │ ├── nikon-e950.jpg │ │ ├── olympus-c960.jpg │ │ ├── olympus-d320l.jpg │ │ ├── ricoh-rdc5300.jpg │ │ ├── sanyo-vpcg250.jpg │ │ ├── sanyo-vpcsx550.jpg │ │ ├── sony-cybershot.jpg │ │ ├── sony-d700.jpg │ │ └── sony-powershota5.jpg │ ├── gps │ │ ├── DSCN0010.jpg │ │ ├── DSCN0012.jpg │ │ ├── DSCN0021.jpg │ │ ├── DSCN0025.jpg │ │ ├── DSCN0027.jpg │ │ ├── DSCN0029.jpg │ │ ├── DSCN0038.jpg │ │ ├── DSCN0040.jpg │ │ ├── DSCN0042.jpg │ │ └── README.txt │ ├── hdr │ │ ├── README.txt │ │ ├── canon_hdr_NO.jpg │ │ ├── canon_hdr_YES.jpg │ │ ├── iphone_hdr_NO.jpg │ │ └── iphone_hdr_YES.jpg │ ├── invalid │ │ ├── README.txt │ │ ├── image00971.jpg │ │ ├── image01088.jpg │ │ ├── image01137.jpg │ │ ├── image01551.jpg │ │ ├── image01713.jpg │ │ ├── image01980.jpg │ │ └── image02206.jpg │ ├── long_description.jpg │ ├── mobile │ │ ├── HMD_Nokia_8.3_5G.jpg │ │ ├── HMD_Nokia_8.3_5G_hdr.jpg │ │ └── jolla.jpg │ ├── orientation │ │ ├── README.txt │ │ ├── landscape_1.jpg │ │ ├── landscape_2.jpg │ │ ├── landscape_3.jpg │ │ ├── landscape_4.jpg │ │ ├── landscape_5.jpg │ │ ├── landscape_6.jpg │ │ ├── landscape_7.jpg │ │ ├── landscape_8.jpg │ │ ├── portrait_1.jpg │ │ ├── portrait_2.jpg │ │ ├── portrait_3.jpg │ │ ├── portrait_4.jpg │ │ ├── portrait_5.jpg │ │ ├── portrait_6.jpg │ │ ├── portrait_7.jpg │ │ └── portrait_8.jpg │ ├── tests │ │ ├── 11-tests.jpg │ │ ├── 22-canon_tags.jpg │ │ ├── 28-hex_value.jpg │ │ ├── 30-type_error.jpg │ │ ├── 32-lens_data.jpeg │ │ ├── 33-type_error.jpg │ │ ├── 35-empty.jpg │ │ ├── 36-memory_error.jpg │ │ ├── 42_IndexError.jpg │ │ ├── 45-gps_ifd.jpg │ │ ├── 46_UnicodeEncodeError.jpg │ │ ├── 67-0_length_string.jpg │ │ ├── 87_OSError.jpg │ │ ├── Xiaomi_Mi_9T_KeyError.jpg │ │ └── nikon_D3100_TypeError.jpg │ └── xmp │ │ ├── BlueSquare.jpg │ │ └── no_exif.jpg ├── jxl │ └── test_0001.jxl ├── raw │ ├── nikon_z_9_high_efficiency_compressed_dx_cropped_max_overexposed.dng │ ├── nikon_z_9_high_efficiency_compressed_dx_cropped_max_overexposed.nef │ └── sony_alpha_a7iii_raw_image.ARW └── tiff │ ├── Arbitro.tiff │ ├── BSG1.tiff │ ├── Crémieux11.tiff │ ├── DudleyLeavittUtah.tiff │ ├── Jobagent.tiff │ ├── Picoawards.tiff │ ├── README.txt │ ├── Rudless.tiff │ └── Tless0.tiff └── test_process_file.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | # 4 space indentation 9 | [*.py] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | # 2 space indentation 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Problem Description** 11 | A clear and concise description of what is wrong. 12 | 13 | **Sample File** 14 | Here is a sample file which can be used to reproduce the problem. 15 | 16 | * [ ] in a .zip or .tar.gz archive => to avoid GitHub removing or altering EXIF data 17 | * [ ] releasable under a Share-Alike license => any contributed file may be added this repository for regression testing. 18 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Run static code analysis. 3 | # 4 | name: Static Analysis 5 | 6 | on: 7 | - push 8 | 9 | jobs: 10 | static-check: 11 | name: Run Static Analysis 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: "3.11" 21 | 22 | - name: Cache dependencies 23 | uses: actions/cache@v3 24 | with: 25 | path: ~/.cache/pip 26 | key: ${{ runner.os }}-dev-${{ hashFiles('pyproject.toml') }} 27 | restore-keys: | 28 | ${{ runner.os }}-dev- 29 | 30 | - name: Install dependencies 31 | run: | 32 | pip install virtualenv==20.31.2 33 | make venv install-all 34 | 35 | - name: Static analysis of the code 36 | run: | 37 | source .venv/bin/activate 38 | make analyze 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Run unit tests. 3 | # 4 | name: Test 5 | 6 | on: 7 | - push 8 | - workflow_dispatch 9 | 10 | jobs: 11 | pytest: 12 | name: Run Tests 13 | runs-on: ubuntu-22.04 14 | timeout-minutes: 30 15 | strategy: 16 | matrix: 17 | python-version: 18 | - "3.7" 19 | - "3.8" 20 | - "3.9" 21 | - "3.10" 22 | - "3.11" 23 | - "3.12" 24 | - "3.13" 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | 33 | - name: Cache dependencies 34 | uses: actions/cache@v4 35 | with: 36 | path: ~/.cache/pip 37 | key: ${{ runner.os }}-test-${{ hashFiles('setup.py') }} 38 | restore-keys: | 39 | ${{ runner.os }}-test- 40 | 41 | - name: Install 42 | run: | 43 | pip install virtualenv 44 | make venv install-test 45 | 46 | - name: Run in debug and color mode 47 | run: | 48 | source .venv/bin/activate 49 | make test-cli 50 | 51 | - name: Compare image processing output 52 | run: | 53 | source .venv/bin/activate 54 | make test-diff 55 | 56 | - name: Run pytest 57 | run: | 58 | source .venv/bin/activate 59 | make test-pytest 60 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.11.13 5 | hooks: 6 | # Run the linter. 7 | - id: ruff 8 | args: [ --fix ] 9 | # Run the formatter. 10 | - id: ruff-format 11 | 12 | - repo: https://github.com/PyCQA/pylint 13 | rev: v3.1.1 14 | hooks: 15 | - id: pylint 16 | name: pylint 17 | entry: pylint 18 | language: system 19 | types: [python] 20 | args: [ 21 | "-j2" 22 | ] 23 | 24 | - repo: https://github.com/pre-commit/mirrors-mypy 25 | rev: v1.16.1 26 | hooks: 27 | - id: mypy 28 | args: [] 29 | exclude: "tests/|examples/|docs/" 30 | additional_dependencies: 31 | - toml 32 | - types-setuptools 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Contributing 3 | ************ 4 | 5 | All contributions are welcome! 6 | 7 | Bug reports, feature requests, bug fixes, documentation updates, sample images, etc ... 8 | Nothing is too small or too big ;-) 9 | 10 | Please be aware that this is a purely spare time project, so don't be offended if it 11 | takes some time to answer. 12 | 13 | Code Contributions 14 | ****************** 15 | 16 | Please start from the ``develop`` branch for any code or documentation contributions. 17 | You may even find the work has already been done... 18 | 19 | Normally the ``master`` branch is only for stable, released code. 20 | 21 | Sample Images 22 | ************* 23 | 24 | Sample images are very important, allowing for validating new features and limiting regressions. 25 | On every build, the library is run on all images. 26 | 27 | The samples are kept in the ``tests/resources`` folder. 28 | -------------------------------------------------------------------------------- /ChangeLog.rst: -------------------------------------------------------------------------------- 1 | EXIF.py Change Log 2 | ################## 3 | 4 | 3.5.1 — 2025-08-23 5 | * Don't raise exception from decode_maker_note() if strict==False (#243) by Nathan Olson 6 | 7 | 3.5.0 — 2025-08-23 8 | * Add support for JPEG XL (#242) by Nathan Olson 9 | * Add Image SubIFDs decoding and Nikon Z 9 sample (#240) by Martins Bruvelis 10 | * Make it easier to add new file types to testing 11 | 12 | 3.4.0 — 2025-08-04 13 | * better typing of serialize module, varius code cleanup 14 | * fix for invalid slice length (#235) 15 | * Add tests and document usage of truncate_tags (#232) 16 | * Add support for files with signature "ftypavif" or "ftypmif1" (#237) by Markus Vogt 17 | * Add basic makernote tags for Sony (#236) 18 | 19 | 3.3.2 — 2025-07-17 20 | * Fix for import error (#230) 21 | 22 | 3.3.1 — 2025-05-12 23 | * Exclude tests from install in pyproject.toml (#229) by Antonio 24 | * Fix for 'quick' instructions in readme 25 | 26 | 3.3.0 — 2025-04-30 27 | * add more makernote tags for Canon (AFInfo), Nikon, Casio, Apple 28 | * Clarify usage of the stop_tag argument 29 | * Add typing to the tag dicts, migrate to Ruff (#225) 30 | * fix for KeyError extracting thumbnail (#227) 31 | * fix for exception on unhandled heic box hdlr (#228) 32 | 33 | 3.2.0 — 2025-04-27 34 | * Add testing using pytest (#214) 35 | * Field types should be easier to use (#213) 36 | * Fix for incoherent thumbnail extraction (#215) 37 | * Add images from DasMoorhuhn in PR #204 (#216) 38 | * Update and add Canon tags 39 | * Use stricter mypy settings 40 | * Fixes for str, bytes typing mismatch (#219) 41 | * Add test image for TypeError on Nikon (#220) 42 | * Rework code structure and organization 43 | 44 | 3.1.0 — 2025-04-25 45 | * Add more typing definitions (#181) 46 | * Put all test files in the repo (#208) 47 | * use pre-commit to run black, isort, pylint, mypy (#209) 48 | * Canon MakerNote: Allow callable to process tag value (#189) by Daan van Gorkum 49 | * Added missing HEIC box names and handling of a TIFF header inside HEIC (#173) by Antti Ketola 50 | * don't let debug logging trigger an exception (#196) by David Bonner 51 | * fix for certain box names not handled, but skipping would generate valid output by Anand Mahesh 52 | * Add DJI makernotes, extract_thumbnail parameter (#168) by Piero Toffanin 53 | * Fix endianess bug while reading DJI makernotes, add Make tag (#169) by Piero Toffanin 54 | * Make CI pass (#178) by Nick Dimitroff 55 | * update pylint and mypy 56 | * fix webp for file conversion by magick (#132) by Marcelo 57 | * Ignore unknown parsers, fix for #160 (#175) by Herbert Poul 58 | * Allow extracting thumbnails with details=False (#170) 59 | * Add option to return built-in Python types (#129) by Étienne Pelletier 60 | 61 | 3.0.0 — 2022-05-08 62 | * **BREAKING CHANGE:** Add type hints, which removes Python2 compatibility 63 | * Update make_string util to clean up bad values (#128) by Étienne Pelletier 64 | * Fix Olympus SpecialMode Unknown Values (#143) by Paul Barton 65 | * Remove coding system from UserComment sequence only if it is valid (#147) by Grzegorz Ruciński 66 | * Fixes to orientation by Mark 67 | * Add some EXIF tags 68 | * Add support for PNG files (#159) by Marco 69 | * Fix for HEIC Unknown Parsers (#153) by Paul Barton 70 | * Handle images that has corrupted headers/tags (#152) by Mahmoud Harmouch 71 | 72 | 2.3.2 — 2020-10-29 73 | * Fixes for HEIC files from Note10+ (#127) by Drew Perttula 74 | * Add missing EXIF OffsetTime tags (#126) by Étienne Pelletier 75 | 76 | 2.3.1 — 2020-08-07 77 | * Fix bug introduced with v2.3.0 in HEIC processing. 78 | 79 | 2.3.0 — 2020-08-03 80 | * Add notice on Python2 EOL 81 | * Modernize code and improve testing, split up some huge functions 82 | * Added support for webp file format (#116) by Grzegorz Ruciński 83 | * Add linting 84 | * Added missing IFD data type; correct spelling mistake (#119) by Piero Toffanin 85 | * Add syntax highlight for README (#117) by John Lin 86 | * Add Python 3.8 to CI (#113) by 2*yo 87 | * make HEIC exif extractor much more compatible (#109) by Tony Guo 88 | * Add black level tag (#108) 89 | * Use list instead of tuple for classifiers (#107) by Florian Preinstorfer 90 | 91 | 2.2.1 — 2020-07-31 92 | * Very minor corrections. 93 | 94 | 2.2.0 — 2019-07-24 95 | * Add support for Python 3.5, 3.6, 3.7 96 | * Drop official support for Python 2.6, 3.2, 3.3 97 | * Fix for string count equals 0 (issue #67) 98 | * Rebasing of struct pull requests: closes #54, closes #60 by Christopher Chavez 99 | * Refactor to use Python's struct module for packing/unpacking by Dave Jones (waveform80) 100 | * Support floating point fields" by Reed Nightingale (reedbn) 101 | * Raw images support by changing Tiff detection by xaumex 102 | * Fix GPS information erroneously None (#96) by Christopher Chavez 103 | * Initial HEIC support (Sam Rushing) 104 | 105 | 2.1.2 — 2015-09-14 106 | * Fix 90 CW (6) and Rotated 90 CCW (8) which were swapped with each other by Mark Hahnenberg 107 | * Catch memory and overflow errors on file seek, print a warning 108 | * Put manufacturers' makernote definitions in separate files 109 | 110 | 2.1.1 — 2015-05-16 111 | * Add a CONTRIBUTING file for Github. 112 | * Add some FujiFilm tags. 113 | * Revert Canon Makernote processing modifications 114 | 115 | 2.1.0 — 2015-05-15 116 | * Bypass empty/unreadable Olympus MakerNote info (issue #42) 117 | * Support Apple Makernote and Apple HDR details by Jesus Cea 118 | * Correcty process the Makernote of some Canon models by Jesus Cea 119 | * Support HDR in Canon cameras by Jesus Cea 120 | 121 | 2.0.2 — 2015-03-29 122 | * Fixed bug when importing as a module (issue #31) 123 | 124 | 2.0.1 — 2014-02-09 125 | * Represent the IFD as a string to fix formatting errors (issue #45) 126 | * Fix unicode errors in python2 (issue #46) 127 | * Fix for tag name backwards compatibility with 1.X series 128 | 129 | 2.0.0 — 2014-11-27 130 | * Drop support for Python 2.5 131 | * Add support for Python 3.2, 3.3 and 3.4 by velis74 132 | * Add Travis testing 133 | * Cleanup some tag definitions 134 | * Fix bug #30 (TypeError on invalid IFD) 135 | * Fix bug #33 (TypeError on invalid output characters) 136 | * Add basic coloring for debug mode 137 | * Add finding XMP tags (experimental, debug only) 138 | * Add some missing Exif tags 139 | * Use stdout for log output 140 | * Experimental support for dumping XMP data 141 | 142 | 1.4.2 — 2013-11-28 143 | * A few new Canon tags 144 | * Python3 fixes by velis74 and leprechaun 145 | * Fix for TypeError (issue #28) 146 | * Pylint & PEP8 fixes 147 | 148 | 1.4.1 — 2013-10-19 149 | * Better version handling 150 | * Better PyPI packaging 151 | 152 | 1.4.0 — 2013-09-28 153 | * Many new tags big thanks to Rodolfo Puig, Paul Barton, Joe Beda 154 | * Do not extract thumbnail in quick mode (issue #19) 155 | * Put tag definitions in separate module 156 | * Add more timing info & version info 157 | 158 | 1.3.3 — 2013-08-03 159 | * Add timing info in debug mode and nicer message format 160 | * Fix for faster processing 161 | 162 | 1.3.2 — 2013-07-31 163 | * Improve PyPI package 164 | * fix for DeprecationWarning: classic int division 165 | * Improvements to debug output 166 | * Add some Nikon makernote tags 167 | 168 | 1.3.1 — 2013-07-29 169 | * More PEP8 & PEP257 improvements 170 | * Better logging 171 | 172 | 1.3.0 — 2013-07-27 173 | * Set default values in case not set (ortsed) 174 | * PEP8 & PEP257 improvements 175 | * Better score in pylint 176 | * Ideas and some code from Samuele Santi's and Peter Reimer's forks 177 | * Replace print with logging 178 | * Package for PyPI 179 | 180 | 1.2.0 — 2013-02-08 181 | * Port to Python 3 by DarkRedman 182 | * Fix endless loop on broken images by Michael Bemmerl 183 | * Rewrite of README.md 184 | * Fixed incoherent copyright notices 185 | 186 | 1.1.0 — 2012-11-30 - all by Gregory Dudek 187 | * Overflow error fixes added (related to 2**31 size) 188 | * GPS tags added. 189 | 190 | 1.0.10 — 2012-09-26 191 | * Add GPS tags 192 | * Add better endian debug info 193 | 194 | 2012-06-13 195 | * Support malformed last IFD by fhats 196 | * Light source, Flash and Metering mode dictionaries by gryfik 197 | 198 | 2008-07-31 199 | * Wikipedia Commons hunt for suitable test case images, 200 | * testing new code additions. 201 | 202 | 2008-07-09 - all by Stephen H. Olson 203 | * Fix a problem with reading MakerNotes out of NEF files. 204 | * Add some more Nikon MakerNote tags. 205 | 206 | 2008-07-08 - all by Stephen H. Olson 207 | * An error check for large tags totally borked MakerNotes. 208 | With Nikon anyway, valid MakerNotes can be pretty big. 209 | * Add error check for a crash caused by nikon_ev_bias being 210 | called with the wrong args. 211 | * Drop any garbage after a null character in string 212 | (patch from Andrew McNabb ). 213 | 214 | 2008-02-12 215 | * Fix crash on invalid MakerNote 216 | * Fix crash on huge Makernote (temp fix) 217 | * Add printIM tag 0xC4A5, needs decoding info 218 | * Add 0x9C9B-F range of tags 219 | * Add a bunch of tag definitions from: 220 | http://owl.phy.queensu.ca/~phil/exiftool/TagNames/EXIF.html 221 | * Add 'strict' variable and command line option 222 | 223 | 2008-01-18 - all by Gunter Ohrner 224 | * Add ``GPSDate`` tag 225 | 226 | 2007-12-12 227 | * Fix quick option on certain image types 228 | * Add note on tag naming in documentation 229 | 230 | 2007-11-30 231 | * Changed -s option to -t 232 | * Put changelog into separate file 233 | 234 | 2007-10-28 235 | * Merged changes from ReimarBauer 236 | * Added command line option for debug, stop 237 | processing on tag. 238 | 239 | 2007-09-27 240 | * Add some Olympus Makernote tags. 241 | 242 | 2007-09-26 - all by Stephen H. Olson 243 | * Don't error out on invalid Olympus 'SpecialMode'. 244 | * Add a few more Olympus/Minolta tags. 245 | 246 | 2007-09-22 - all by Stephen H. Olson 247 | * Don't error on invalid string 248 | * Improved Nikon MakerNote support 249 | 250 | 2007-05-03 - all by Martin Stone 251 | * Fix for inverted detailed flag and Photoshop header 252 | 253 | 2007-03-24 254 | * Can now ignore MakerNotes Tags for faster processing. 255 | 256 | 2007-01-18 257 | * Fixed a couple errors and assuming maintenance of the library. 258 | 259 | 2006-08-04 all by Reimar Bauer 260 | * Added an optional parameter name to process_file and dump_IFD. Using this 261 | parameter the loop is breaked after that tag_name is processed. 262 | * some PEP8 changes 263 | 264 | 265 | Original Notices 266 | **************** 267 | 268 | Contains code from "exifdump.py" originally written by Thierry Bousch 269 | and released into the public domain. 270 | 271 | Updated and turned into general-purpose library by Gene Cash 272 | 273 | Patch Contributors: 274 | * Simon J. Gerraty 275 | s2n fix & orientation decode 276 | * John T. Riedl 277 | Added support for newer Nikon type 3 Makernote format for D70 and some 278 | other Nikon cameras. 279 | * Joerg Schaefer 280 | Fixed subtle bug when faking an EXIF header, which affected maker notes 281 | using relative offsets, and a fix for Nikon D100. 282 | 283 | 2004-02-15 CEC 284 | * Finally fixed bit shift warning by converting Y to 0L. 285 | 286 | 2003-11-30 CEC 287 | * Fixed problem with canon_decode_tag() not creating an 288 | IFD_Tag() object. 289 | 290 | 2002-01-26 CEC 291 | * Added ability to extract TIFF thumbnails. 292 | * Added Nikon, Fujifilm, Casio MakerNotes. 293 | 294 | 2002-01-25 CEC 295 | * Discovered JPEG thumbnail in Olympus TIFF MakerNote. 296 | 297 | 2002-01-23 CEC 298 | * Trimmed nulls from end of string values. 299 | 300 | 2002-01-20 CEC Added MakerNote processing logic. 301 | * Added Olympus MakerNote. 302 | * Converted data structure to single-level dictionary, avoiding 303 | tag name collisions by prefixing with IFD name. This makes 304 | it much easier to use. 305 | 306 | 2002-01-19 CEC Added ability to read TIFFs and JFIF-format JPEGs. 307 | * Added ability to extract JPEG formatted thumbnail. 308 | * Added ability to read GPS IFD (not tested). 309 | * Converted IFD data structure to dictionaries indexed by tag name. 310 | * Factored into library returning dictionary of IFDs plus thumbnail, if any. 311 | 312 | 2002-01-17 CEC Discovered code on web. 313 | * Commented everything. 314 | * Made small code improvements. 315 | * Reformatted for readability. 316 | 317 | 1999-08-21 TB 318 | * Last update by Thierry Bousch to his code. 319 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2002-2007 Gene Cash 3 | Copyright (c) 2007-2025 Ianaré Sévi and contributors 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | 3. Neither the name of the authors nor the names of its contributors 18 | may be used to endorse or promote products derived from this 19 | software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.rst 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | ifneq (,$(wildcard /.dockerenv)) 3 | PYTHON_BIN := /usr/local/bin/python3 4 | PIP_BIN := /usr/local/bin/pip3 5 | PRE_COMMIT_BIN := ~/.local/bin/pre-commit 6 | PYTEST_BIN := $(PYTHON_BIN) -m pytest 7 | TWINE_BIN := ~/.local/bin/twine 8 | PIP_INSTALL := $(PIP_BIN) install --progress-bar=off --user 9 | else 10 | VENV_DIR := ./.venv 11 | PYTHON_BIN := $(VENV_DIR)/bin/python3 12 | PIP_BIN := $(VENV_DIR)/bin/pip3 13 | PRE_COMMIT_BIN := $(VENV_DIR)/bin/pre-commit 14 | PYTEST_BIN := $(PYTHON_BIN) -m pytest 15 | TWINE_BIN := $(VENV_DIR)/bin/twine 16 | PIP_INSTALL := $(PIP_BIN) install --progress-bar=off 17 | endif 18 | 19 | # If exifread is installed locally (e.g. pip install -e .), use it, else fallback to local bin 20 | EXIF_PY := $(if $(shell which EXIF.py),EXIF.py,./EXIF.py) 21 | 22 | # Find images, support multiple case insensitive extensions and file names with spaces 23 | FIND_IMAGES := find tests/resources -type f -regextype posix-egrep -not -iregex ".*\.(txt|rst)" -print0 | LC_COLLATE=C sort -fz | xargs -0 24 | 25 | 26 | .PHONY: help 27 | all: help 28 | 29 | venv: ## Set up the virtual environment 30 | virtualenv -p python3 $(VENV_DIR) 31 | 32 | test-cli: ## Run exifread on all sample images 33 | $(FIND_IMAGES) $(EXIF_PY) -dc 34 | 35 | test-diff: ## Run and compare exif dump 36 | $(FIND_IMAGES) $(EXIF_PY) > tests/resources/dump_test.txt 37 | diff -Zu --color --suppress-common-lines tests/resources/dump.txt tests/resources/dump_test.txt 38 | 39 | test-pytest: ## Run pytest 40 | $(PYTEST_BIN) -v 41 | 42 | test: test-cli test-diff test-pytest ## Run all tests 43 | 44 | analyze: ## Run all static analysis tools 45 | $(PRE_COMMIT_BIN) run --all 46 | 47 | install-dev: ## Install with all development requirements 48 | $(PIP_INSTALL) -U -e .[dev] 49 | 50 | install-test: ## Install with all testing requirements 51 | $(PIP_INSTALL) -U -e .[test] 52 | 53 | install-all: ## Install with all requirements 54 | $(PIP_INSTALL) -U -e .[test,dev] 55 | 56 | install: ## Install with basic requirements 57 | $(PIP_INSTALL) -U -e . 58 | 59 | build: ## build distribution 60 | rm -fr ./dist 61 | $(PYTHON_BIN) -m build 62 | $(TWINE_BIN) check --strict ./dist/* 63 | 64 | publish-test: build ## Publish to test PyPI 65 | $(TWINE_BIN) upload --repository testpypi dist/* 66 | 67 | publish: build ## Publish to test PyPI 68 | $(TWINE_BIN) upload dist/* 69 | 70 | help: Makefile 71 | @echo 72 | @echo "Choose a command to run:" 73 | @echo 74 | @grep --no-filename -E '^[a-zA-Z_%-]+:.*?## .*$$' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-30s\033[0m %s\n", $$1, $$2}' 75 | @echo 76 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | EXIF.py 3 | ******* 4 | 5 | .. image:: https://img.shields.io/github/license/ianare/exif-py 6 | :target: https://opensource.org/license/bsd-3-clause 7 | :alt: BSD-3-clause 8 | .. image:: https://img.shields.io/pypi/v/ExifRead 9 | :target: https://pypi.org/project/ExifRead 10 | :alt: PyPi 11 | .. image:: https://img.shields.io/pypi/dm/ExifRead 12 | :target: https://pypi.org/project/ExifRead 13 | :alt: BSD-3-clause 14 | .. image:: http://www.mypy-lang.org/static/mypy_badge.svg 15 | :target: http://mypy-lang.org/ 16 | :alt: Checked with mypy 17 | .. image:: https://img.shields.io/github/actions/workflow/status/ianare/exif-py/test.yml 18 | :target: https://github.com/ianare/exif-py 19 | :alt: Tests 20 | 21 | | 22 | 23 | Easy to use Python module to extract Exif metadata from digital image files. 24 | 25 | Pure Python, lightweight, no dependencies. 26 | 27 | Supported formats: TIFF, JPEG, JPEG XL, PNG, Webp, HEIC, RAW 28 | 29 | 30 | Compatibility 31 | ************* 32 | 33 | EXIF.py is tested and officially supported on Python 3.7 to 3.13 34 | 35 | 36 | Installation 37 | ************ 38 | 39 | Stable Version 40 | ============== 41 | The recommended process is to install the `PyPI package `_, 42 | as it allows easily staying up to date:: 43 | 44 | $ pip install exifread 45 | 46 | See the `pip documentation `_ for more info. 47 | 48 | EXIF.py is mature software and strives for stability. 49 | 50 | Development Version 51 | =================== 52 | 53 | After cloning the repo, use the provided Makefile:: 54 | 55 | make venv install-all 56 | 57 | Which will create a virtual environment and install development dependencies. 58 | 59 | Usage 60 | ***** 61 | 62 | Command line 63 | ============ 64 | 65 | Some examples:: 66 | 67 | EXIF.py image1.jpg 68 | EXIF.py -dc image1.jpg image2.tiff 69 | find ~/Pictures -name "*.jpg" -o -name "*.tiff" | xargs EXIF.py 70 | 71 | Show command line options:: 72 | 73 | EXIF.py -h 74 | 75 | Python Script 76 | ============= 77 | 78 | .. code-block:: python 79 | 80 | import exifread 81 | 82 | # Open image file for reading (must be in binary mode) 83 | with open(file_path, "rb") as file_handle: 84 | 85 | # Return Exif tags 86 | tags = exifread.process_file(file_handle) 87 | 88 | *Note:* To use this library in your project as a Git submodule, you should:: 89 | 90 | from import exifread 91 | 92 | Returned tags will be a dictionary mapping names of Exif tags to their 93 | values in the file named by ``file_path``. 94 | You can process the tags as you wish. In particular, you can iterate through all the tags with: 95 | 96 | .. code-block:: python 97 | 98 | for tag, value in tags.items(): 99 | if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename', 'EXIF MakerNote'): 100 | print(f"Key: {tag}, value {value}") 101 | 102 | An ``if`` statement is used to avoid printing out a few of the tags that tend to be long or boring. 103 | 104 | The tags dictionary will include keys for all of the usual Exif tags, and will also include keys for 105 | Makernotes used by some cameras, for which we have a good specification. 106 | 107 | Note that the dictionary keys are the IFD name followed by the tag name. For example:: 108 | 109 | 'EXIF DateTimeOriginal', 'Image Orientation', 'MakerNote FocusMode' 110 | 111 | 112 | Tag Descriptions 113 | **************** 114 | 115 | Tags are divided into these main categories: 116 | 117 | - ``Image``: information related to the main image (IFD0 of the Exif data). 118 | - ``Thumbnail``: information related to the thumbnail image, if present (IFD1 of the Exif data). 119 | - ``EXIF``: Exif information (sub-IFD). 120 | - ``GPS``: GPS information (sub-IFD). 121 | - ``Interoperability``: Interoperability information (sub-IFD). 122 | - ``MakerNote``: Manufacturer specific information. There are no official published references for these tags. 123 | 124 | 125 | Processing Options 126 | ****************** 127 | 128 | These options can be used both in command line mode and within a script. 129 | 130 | Faster Processing 131 | ================= 132 | 133 | Don't process makernote tags, don't extract the thumbnail image (if any). 134 | 135 | Pass the ``-q`` or ``--quick`` command line arguments, or as: 136 | 137 | .. code-block:: python 138 | 139 | tags = exifread.process_file( 140 | file_handle, details=False, extract_thumbnail=False 141 | ) 142 | 143 | To process makernotes only, without extracting the thumbnail image (if any): 144 | 145 | .. code-block:: python 146 | 147 | tags = exifread.process_file( 148 | file_handle, details=True, extract_thumbnail=False 149 | ) 150 | 151 | To extract the thumbnail image (if any), without processing makernotes: 152 | 153 | .. code-block:: python 154 | 155 | tags = exifread.process_file( 156 | file_handle, details=False, extract_thumbnail=True 157 | ) 158 | 159 | Stop at a Given Tag 160 | =================== 161 | 162 | To stop processing the file after a specified tag is retrieved. 163 | 164 | Pass the ``-t TAG`` or ``--stop-tag TAG`` argument, or as: 165 | 166 | .. code-block:: python 167 | 168 | tags = exifread.process_file(file_handle, stop_tag='TAG') 169 | 170 | where ``TAG`` is a valid tag name without the IFD, ex ``'DateTimeOriginal'``. 171 | 172 | *The two above options are useful to speed up processing of large numbers of files.* 173 | 174 | Strict Processing 175 | ================= 176 | 177 | Return an error on invalid tags instead of silently ignoring. 178 | 179 | Pass the ``-s`` or ``--strict`` argument, or as: 180 | 181 | .. code-block:: python 182 | 183 | tags = exifread.process_file(file_handle, strict=True) 184 | 185 | Built-in Types 186 | ============== 187 | 188 | For easier serialization and programmatic use, this option returns a dictionary with values in built-in Python types 189 | (int, float, str, bytes, list, None) instead of `IfdTag` objects. 190 | 191 | Pass the ``-b`` or ``--builtin`` argument, or as: 192 | 193 | .. code-block:: python 194 | 195 | tags = exifread.process_file(file_handle, builtin_types=True) 196 | 197 | For direct JSON serialization, combine this option with ``details=False`` to avoid bytes in the output: 198 | 199 | .. code-block:: python 200 | 201 | json.dumps( 202 | exifread.process_file(file_handle, details=False, builtin_types=True) 203 | ) 204 | 205 | Usage Example 206 | ============= 207 | 208 | This example shows how to use the library to correct the orientation of an image 209 | (using Pillow for the transformation) before e.g. displaying it. 210 | 211 | .. code-block:: python 212 | 213 | import exifread 214 | from PIL import Image 215 | import logging 216 | 217 | def _read_img_and_correct_exif_orientation(path): 218 | im = Image.open(path) 219 | tags = {} 220 | with open(path, "rb") as file_handle: 221 | tags = exifread.process_file(file_handle, details=False) 222 | 223 | if "Image Orientation" in tags: 224 | orientation = tags["Image Orientation"] 225 | logging.basicConfig(level=logging.DEBUG) 226 | logging.debug("Orientation: %s (%s)", orientation, orientation.values) 227 | val = orientation.values 228 | if 2 in val: 229 | val += [4, 3] 230 | if 5 in val: 231 | val += [4, 6] 232 | if 7 in val: 233 | val += [4, 8] 234 | if 3 in val: 235 | logging.debug("Rotating by 180 degrees.") 236 | im = im.transpose(Image.ROTATE_180) 237 | if 4 in val: 238 | logging.debug("Mirroring horizontally.") 239 | im = im.transpose(Image.FLIP_TOP_BOTTOM) 240 | if 6 in val: 241 | logging.debug("Rotating by 270 degrees.") 242 | im = im.transpose(Image.ROTATE_270) 243 | if 8 in val: 244 | logging.debug("Rotating by 90 degrees.") 245 | im = im.transpose(Image.ROTATE_90) 246 | return im 247 | 248 | 249 | License 250 | ******* 251 | 252 | Copyright © 2002-2007 Gene Cash 253 | 254 | Copyright © 2007-2025 Ianaré Sévi and contributors 255 | 256 | A **huge** thanks to all the contributors over the years! 257 | 258 | Originally written by Gene Cash & Thierry Bousch. 259 | 260 | Available as open source under the terms of the **BSD-3-Clause license**. 261 | 262 | See the LICENSE file for details. 263 | -------------------------------------------------------------------------------- /exifread/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read Exif metadata from image files 3 | Supported formats: TIFF, JPEG, PNG, Webp, HEIC 4 | """ 5 | 6 | from typing import Any, BinaryIO, Dict 7 | 8 | from exifread.core.exceptions import ExifNotFound, InvalidExif 9 | from exifread.core.exif_header import ExifHeader 10 | from exifread.core.find_exif import determine_type, get_endian_str 11 | from exifread.core.xmp import find_xmp_data 12 | from exifread.exif_log import get_logger 13 | from exifread.serialize import convert_types 14 | from exifread.tags import DEFAULT_STOP_TAG 15 | 16 | __version__ = "3.5.1" 17 | 18 | logger = get_logger() 19 | 20 | 21 | def _extract_xmp_data(hdr: ExifHeader, fh: BinaryIO): 22 | # Easy we already have them 23 | xmp_tag = hdr.tags.get("Image ApplicationNotes") 24 | if xmp_tag: 25 | logger.debug("XMP present in Exif") 26 | xmp_bytes = bytes(xmp_tag.values) 27 | # We need to look in the entire file for the XML 28 | else: 29 | xmp_bytes = find_xmp_data(fh) 30 | if xmp_bytes: 31 | hdr.parse_xmp(xmp_bytes) 32 | 33 | 34 | def process_file( 35 | fh: BinaryIO, 36 | stop_tag: str = DEFAULT_STOP_TAG, 37 | details=True, 38 | strict=False, 39 | debug=False, 40 | truncate_tags=True, 41 | auto_seek=True, 42 | extract_thumbnail=True, 43 | builtin_types=False, 44 | ) -> Dict[str, Any]: 45 | """ 46 | Process an image file to extract EXIF metadata. 47 | 48 | This is the function that has to deal with all the arbitrary nasty bits 49 | of the EXIF standard. 50 | 51 | :param fh: the file to process, must be opened in binary mode. 52 | :param stop_tag: Stop processing when the given tag is retrieved. 53 | :param details: If `True`, process MakerNotes. 54 | :param strict: If `True`, raise exceptions on errors. 55 | :param debug: Output debug information. 56 | :param truncate_tags: If `True`, truncate the `printable` tag output. 57 | There is no effect on tag `values`. 58 | :param auto_seek: If `True`, automatically `seek` to the start of the file. 59 | :param extract_thumbnail: If `True`, extract the JPEG thumbnail. 60 | The thumbnail is not always present in the EXIF metadata. 61 | :param builtin_types: If `True`, convert tags to standard Python types. 62 | 63 | :returns: A `dict` containing the EXIF metadata. 64 | The keys are a string in the format `"IFD_NAME TAG_NAME"`. 65 | If `builtin_types` is `False`, the value will be a `IfdTag` class, or bytes. 66 | IF `builtin_types` is `True`, the value will be a standard Python type. 67 | """ 68 | 69 | if auto_seek: 70 | fh.seek(0) 71 | 72 | try: 73 | offset, endian_bytes, fake_exif = determine_type(fh) 74 | except ExifNotFound as err: 75 | logger.warning(err) 76 | return {} 77 | except InvalidExif as err: 78 | logger.debug(err) 79 | return {} 80 | 81 | endian_str, endian_type = get_endian_str(endian_bytes) 82 | # deal with the EXIF info we found 83 | logger.debug("Endian format is %s (%s)", endian_str, endian_type) 84 | 85 | hdr = ExifHeader( 86 | fh, endian_str, offset, fake_exif, strict, debug, details, truncate_tags 87 | ) 88 | thumb_ifd = 0 89 | ctr = 0 90 | for ifd in hdr.list_ifd(): 91 | if ctr == 0: 92 | ifd_name = "Image" 93 | elif ctr == 1: 94 | ifd_name = "Thumbnail" 95 | thumb_ifd = ifd 96 | else: 97 | ifd_name = "IFD %d" % ctr 98 | logger.debug("IFD %d (%s) at offset %s:", ctr, ifd_name, ifd) 99 | hdr.dump_ifd(ifd=ifd, ifd_name=ifd_name, stop_tag=stop_tag) 100 | ctr += 1 101 | # EXIF IFD 102 | exif_off = hdr.tags.get("Image ExifOffset") 103 | if exif_off: 104 | logger.debug("Exif SubIFD at offset %s:", exif_off.values[0]) 105 | hdr.dump_ifd(ifd=exif_off.values[0], ifd_name="EXIF", stop_tag=stop_tag) 106 | 107 | # EXIF SubIFD 108 | sub_ifds = hdr.tags.get("Image SubIFDs") 109 | if details and sub_ifds: 110 | for subifd_id, subifd_offset in enumerate(sub_ifds.values): 111 | logger.debug("Exif SubIFD%d at offset %d:", subifd_id, subifd_offset) 112 | hdr.dump_ifd( 113 | ifd=subifd_offset, ifd_name=f"EXIF SubIFD{subifd_id}", stop_tag=stop_tag 114 | ) 115 | 116 | # deal with MakerNote contained in EXIF IFD 117 | # (Some apps use MakerNote tags but do not use a format for which we 118 | # have a description, do not process these). 119 | if details and "EXIF MakerNote" in hdr.tags and "Image Make" in hdr.tags: 120 | try: 121 | hdr.decode_maker_note() 122 | except ValueError as err: 123 | if not strict: 124 | logger.debug("Failed to decode EXIF MakerNote: %s", str(err)) 125 | else: 126 | raise err 127 | 128 | # extract thumbnails 129 | if thumb_ifd and extract_thumbnail: 130 | hdr.extract_tiff_thumbnail(thumb_ifd) 131 | hdr.extract_jpeg_thumbnail() 132 | 133 | # parse XMP tags (experimental) 134 | if debug and details: 135 | _extract_xmp_data(hdr=hdr, fh=fh) 136 | 137 | if builtin_types: 138 | return convert_types(hdr.tags) 139 | 140 | return hdr.tags 141 | -------------------------------------------------------------------------------- /exifread/__main__.py: -------------------------------------------------------------------------------- 1 | """Main entrypoint for the module.""" 2 | 3 | from exifread.cli import main 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /exifread/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # 4 | # Library to extract Exif information from digital camera image files. 5 | # https://github.com/ianare/exif-py 6 | # 7 | # 8 | # Copyright (c) 2002-2007 Gene Cash 9 | # Copyright (c) 2007-2025 Ianaré Sévi and contributors 10 | # 11 | # See LICENSE.txt file for licensing information 12 | # See ChangeLog.rst file for all contributors and changes 13 | # 14 | 15 | """ 16 | Runs Exif tag extraction in command line. 17 | """ 18 | 19 | import argparse 20 | import sys 21 | import timeit 22 | 23 | from exifread import __version__, exif_log, process_file 24 | from exifread.core.exceptions import ExifError 25 | from exifread.tags.fields import FIELD_DEFINITIONS 26 | 27 | logger = exif_log.get_logger() 28 | 29 | 30 | def get_args() -> argparse.Namespace: 31 | parser = argparse.ArgumentParser( 32 | prog="EXIF.py", description="Extract EXIF information from digital image files." 33 | ) 34 | parser.add_argument( 35 | "files", 36 | metavar="FILE", 37 | type=str, 38 | nargs="+", 39 | help="files to process", 40 | ) 41 | parser.add_argument( 42 | "-v", 43 | "--version", 44 | action="version", 45 | version="EXIF.py Version %s on Python%s" % (__version__, sys.version_info[0]), 46 | help="Display version information and exit", 47 | ) 48 | parser.add_argument( 49 | "-q", 50 | "--quick", 51 | action="store_false", 52 | dest="detailed", 53 | help="Do not process MakerNotes and do not extract thumbnails", 54 | ) 55 | parser.add_argument( 56 | "-t", 57 | "--tag", 58 | type=str, 59 | dest="stop_tag", 60 | help="Stop processing when this tag is retrieved.", 61 | ) 62 | parser.add_argument( 63 | "-s", 64 | "--strict", 65 | action="store_true", 66 | dest="strict", 67 | help="Run in strict mode (stop on errors).", 68 | ) 69 | parser.add_argument( 70 | "-b", 71 | "--builtin", 72 | action="store_true", 73 | dest="builtin_types", 74 | help="Convert IfdTag values to built-in Python variable types", 75 | ) 76 | parser.add_argument( 77 | "-d", 78 | "--debug", 79 | action="store_true", 80 | dest="debug", 81 | help="Run in debug mode (display extra info).", 82 | ) 83 | parser.add_argument( 84 | "-c", 85 | "--color", 86 | action="store_true", 87 | dest="color", 88 | help="Output in color (only works with debug on POSIX).", 89 | ) 90 | args = parser.parse_args() 91 | return args 92 | 93 | 94 | def run_cli(args: argparse.Namespace) -> None: 95 | """Extract tags based on options (args).""" 96 | 97 | exif_log.setup_logger(args.debug, args.color) 98 | 99 | # output info for each file 100 | for filename in args.files: 101 | # avoid errors when printing to console 102 | escaped_fn = filename.encode( 103 | sys.getfilesystemencoding(), "surrogateescape" 104 | ).decode() 105 | 106 | file_start = timeit.default_timer() 107 | try: 108 | with open(escaped_fn, "rb") as img_file: 109 | logger.info("Opening: %s", escaped_fn) 110 | 111 | tag_start = timeit.default_timer() 112 | # get the tags 113 | data = process_file( 114 | img_file, 115 | stop_tag=args.stop_tag, 116 | details=args.detailed, 117 | strict=args.strict, 118 | debug=args.debug, 119 | extract_thumbnail=args.detailed, 120 | builtin_types=args.builtin_types, 121 | ) 122 | tag_stop = timeit.default_timer() 123 | 124 | except IOError: 125 | logger.error("'%s' is unreadable", escaped_fn) 126 | continue 127 | 128 | if not data: 129 | logger.warning("No EXIF information found") 130 | print() 131 | continue 132 | 133 | if "JPEGThumbnail" in data: 134 | logger.info("File has JPEG thumbnail") 135 | del data["JPEGThumbnail"] 136 | if "TIFFThumbnail" in data: 137 | logger.info("File has TIFF thumbnail") 138 | del data["TIFFThumbnail"] 139 | 140 | for field in sorted(data): 141 | value = data[field] 142 | try: 143 | if args.builtin_types: 144 | logger.info("%s (%s): %r", field, type(value).__name__, value) 145 | else: 146 | logger.info( 147 | "%s (%s): %s", 148 | field, 149 | FIELD_DEFINITIONS[value.field_type][1], 150 | value.printable, 151 | ) 152 | except (ExifError, ValueError): 153 | logger.error("%s: %s", field, str(value)) 154 | 155 | file_stop = timeit.default_timer() 156 | 157 | logger.debug("Tags processed in %s seconds", tag_stop - tag_start) 158 | logger.debug("File processed in %s seconds", file_stop - file_start) 159 | print() 160 | 161 | 162 | def main() -> None: 163 | run_cli(get_args()) 164 | 165 | 166 | if __name__ == "__main__": 167 | main() 168 | -------------------------------------------------------------------------------- /exifread/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/exifread/core/__init__.py -------------------------------------------------------------------------------- /exifread/core/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exception classes.""" 2 | 3 | 4 | class ExifError(Exception): 5 | """Base class for all errors.""" 6 | 7 | 8 | class InvalidExif(ExifError): 9 | """The EXIF is invalid.""" 10 | 11 | 12 | class ExifNotFound(ExifError): 13 | """The EXIF could not be found.""" 14 | -------------------------------------------------------------------------------- /exifread/core/find_exif.py: -------------------------------------------------------------------------------- 1 | """Utilities to find the EXIF offset and endian.""" 2 | 3 | import struct 4 | from typing import BinaryIO, Dict, Tuple 5 | 6 | from exifread.core.exceptions import ExifNotFound, InvalidExif 7 | from exifread.core.heic import HEICExifFinder, find_heic_tiff 8 | from exifread.core.jpeg import find_jpeg_exif 9 | from exifread.core.jxl import JXLExifFinder 10 | from exifread.core.utils import ord_ 11 | from exifread.exif_log import get_logger 12 | 13 | logger = get_logger() 14 | 15 | 16 | ENDIAN_TYPES: Dict[str, str] = { 17 | "I": "Intel", 18 | "M": "Motorola", 19 | "\x01": "Adobe Ducky", 20 | "b": "XMP/Adobe unknown", 21 | } 22 | 23 | 24 | def get_endian_str(endian_bytes) -> Tuple[str, str]: 25 | endian_str = chr(ord_(endian_bytes[0])) 26 | return endian_str, ENDIAN_TYPES.get(endian_str, "Unknown") 27 | 28 | 29 | def find_tiff_exif(fh: BinaryIO) -> Tuple[int, bytes]: 30 | logger.debug("TIFF format recognized in data[0:2]") 31 | fh.seek(0) 32 | endian = fh.read(1) 33 | fh.read(1) 34 | offset = 0 35 | return offset, endian 36 | 37 | 38 | def find_webp_exif(fh: BinaryIO) -> Tuple[int, bytes]: 39 | logger.debug("WebP format recognized in data[0:4], data[8:12]") 40 | # file specification: https://developers.google.com/speed/webp/docs/riff_container 41 | data = fh.read(5) 42 | if data[0:4] == b"VP8X" and data[4] & 8: 43 | # https://developers.google.com/speed/webp/docs/riff_container#extended_file_format 44 | fh.seek(13, 1) 45 | while True: 46 | data = fh.read(8) # Chunk FourCC (32 bits) and Chunk Size (32 bits) 47 | if len(data) != 8: 48 | raise InvalidExif("Invalid webp file chunk header.") 49 | if data[0:4] == b"EXIF": 50 | fh.seek(6, 1) 51 | offset = fh.tell() 52 | endian = fh.read(1) 53 | return offset, endian 54 | size = struct.unpack(" Tuple[int, bytes]: 60 | logger.debug("PNG format recognized in data[0:8]=%s", data[:8].hex()) 61 | fh.seek(8) 62 | 63 | while True: 64 | data = fh.read(8) 65 | chunk = data[4:8] 66 | logger.debug("PNG found chunk %s", chunk.decode("ascii")) 67 | 68 | if chunk in (b"", b"IEND"): 69 | break 70 | if chunk == b"eXIf": 71 | offset = fh.tell() 72 | return offset, fh.read(1) 73 | 74 | chunk_size = int.from_bytes(data[:4], "big") 75 | fh.seek(fh.tell() + chunk_size + 4) 76 | 77 | raise ExifNotFound("PNG file does not have exif data.") 78 | 79 | 80 | def find_jxl_exif(fh: BinaryIO) -> Tuple[int, bytes]: 81 | logger.debug("JPEG XL format recognized in data[0:12]") 82 | 83 | fh.seek(0) 84 | jxl = JXLExifFinder(fh) 85 | offset, endian = jxl.find_exif() 86 | if offset > 0: 87 | return offset, endian 88 | 89 | raise ExifNotFound("JPEG XL file does not have exif data.") 90 | 91 | 92 | def determine_type(fh: BinaryIO) -> Tuple[int, bytes, int]: 93 | # by default do not fake an EXIF beginning 94 | fake_exif = 0 95 | 96 | data = fh.read(12) 97 | if data[0:2] in [b"II", b"MM"]: 98 | # it's a TIFF file 99 | offset, endian = find_tiff_exif(fh) 100 | elif data[4:12] in [b"ftypheic", b"ftypavif", b"ftypmif1"]: 101 | fh.seek(0) 102 | heic = HEICExifFinder(fh) 103 | offset, endian = heic.find_exif() 104 | if offset == 0: 105 | offset, endian = find_heic_tiff(fh) 106 | # It's a HEIC file with a TIFF header 107 | elif data[0:4] == b"RIFF" and data[8:12] == b"WEBP": 108 | offset, endian = find_webp_exif(fh) 109 | elif data[0:2] == b"\xff\xd8": 110 | # it's a JPEG file 111 | offset, endian, fake_exif = find_jpeg_exif(fh, data, fake_exif) 112 | elif data[0:8] == b"\x89PNG\r\n\x1a\n": 113 | offset, endian = find_png_exif(fh, data) 114 | elif data == b"\0\0\0\x0cJXL\x20\x0d\x0a\x87\x0a": 115 | offset, endian = find_jxl_exif(fh) 116 | else: 117 | raise ExifNotFound("File format not recognized.") 118 | return offset, endian, fake_exif 119 | -------------------------------------------------------------------------------- /exifread/core/heic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Find Exif data in an HEIC file. 3 | 4 | As of 2019, the latest standard seems to be "ISO/IEC 14496-12:2015" 5 | There are many different related standards. (quicktime, mov, mp4, etc...) 6 | See https://en.wikipedia.org/wiki/ISO_base_media_file_format for more details. 7 | 8 | We parse just enough of the ISO format to locate the Exif data in the file. 9 | Inside the 'meta' box are two directories we need: 10 | 1) the 'iinf' box contains 'infe' records, we look for the item_id for 'Exif'. 11 | 2) once we have the item_id, we find a matching entry in the 'iloc' box, which 12 | gives us position and size information. 13 | """ 14 | 15 | import struct 16 | from typing import Any, BinaryIO, Callable, Dict, List, Optional, Tuple 17 | 18 | from exifread.core.exceptions import ExifError, InvalidExif 19 | from exifread.exif_log import get_logger 20 | 21 | logger = get_logger() 22 | 23 | 24 | def find_heic_tiff(fh: BinaryIO) -> Tuple[int, bytes]: 25 | """ 26 | Look for TIFF header in HEIC files. 27 | 28 | In some HEIC files, the Exif offset is 0, 29 | and yet there is a plain TIFF header near end of the file. 30 | """ 31 | 32 | data = fh.read(4) 33 | if data[0:2] in [b"II", b"MM"] and data[2] == 42 and data[3] == 0: 34 | offset = fh.tell() - 4 35 | fh.seek(offset) 36 | endian = data[0:2] 37 | offset = fh.tell() 38 | logger.debug("Found TIFF header in Exif, offset = %0xH", offset) 39 | else: 40 | raise InvalidExif( 41 | "Exif pointer to zeros, but found " 42 | + str(data) 43 | + " instead of a TIFF header." 44 | ) 45 | 46 | return offset, endian 47 | 48 | 49 | class BoxVersion(ExifError): 50 | """Wrong box version.""" 51 | 52 | 53 | class BadSize(ExifError): 54 | """Wrong box size.""" 55 | 56 | 57 | class Box: 58 | """A HEIC Box.""" 59 | 60 | version = 0 61 | minor_version = 0 62 | item_count = 0 63 | size = 0 64 | after = 0 65 | pos = 0 66 | compat: List[bytes] = [] 67 | base_offset = 0 68 | # this is full of boxes, but not in a predictable order. 69 | subs: Dict[str, "Box"] = {} 70 | locs: Dict = {} 71 | exif_infe: Optional["Box"] = None 72 | item_id = 0 73 | item_type = b"" 74 | item_name = b"" 75 | item_protection_index = 0 76 | major_brand = b"" 77 | offset_size = 0 78 | length_size = 0 79 | base_offset_size = 0 80 | index_size = 0 81 | flags = 0 82 | name: str 83 | 84 | def __init__(self, name: str) -> None: 85 | self.name = name 86 | 87 | def __repr__(self) -> str: 88 | return "" % self.name 89 | 90 | def set_sizes(self, offset: int, length: int, base_offset: int, index: int) -> None: 91 | self.offset_size = offset 92 | self.length_size = length 93 | self.base_offset_size = base_offset 94 | self.index_size = index 95 | 96 | def set_full(self, vflags: int) -> None: 97 | """ 98 | ISO boxes come in 'old' and 'full' variants. 99 | The 'full' variant contains version and flags information. 100 | """ 101 | self.version = vflags >> 24 102 | self.flags = vflags & 0x00FFFFFF 103 | 104 | 105 | class HEICExifFinder: 106 | """Find HEIC EXIF tags.""" 107 | 108 | file_handle: BinaryIO 109 | 110 | def __init__(self, file_handle: BinaryIO) -> None: 111 | self.file_handle = file_handle 112 | 113 | def get(self, nbytes: int) -> bytes: 114 | read = self.file_handle.read(nbytes) 115 | if not read: 116 | raise EOFError 117 | if len(read) != nbytes: 118 | msg = "get(nbytes={nbytes}) found {read} bytes at position {pos}".format( 119 | nbytes=nbytes, read=len(read), pos=self.file_handle.tell() 120 | ) 121 | raise BadSize(msg) 122 | return read 123 | 124 | def get16(self) -> int: 125 | return struct.unpack(">H", self.get(2))[0] 126 | 127 | def get32(self) -> int: 128 | return struct.unpack(">L", self.get(4))[0] 129 | 130 | def get64(self) -> int: 131 | return struct.unpack(">Q", self.get(8))[0] 132 | 133 | def get_int4x2(self) -> tuple: 134 | num = struct.unpack(">B", self.get(1))[0] 135 | num0 = num >> 4 136 | num1 = num & 0xF 137 | return num0, num1 138 | 139 | def get_int(self, size: int) -> int: 140 | """some fields have variant-sized data.""" 141 | if size == 2: 142 | return self.get16() 143 | if size == 4: 144 | return self.get32() 145 | if size == 8: 146 | return self.get64() 147 | if size == 0: 148 | return 0 149 | raise BadSize(size) 150 | 151 | def get_string(self) -> bytes: 152 | read = [] 153 | while 1: 154 | char = self.get(1) 155 | if char == b"\x00": 156 | break 157 | read.append(char) 158 | return b"".join(read) 159 | 160 | def next_box(self) -> Box: 161 | pos = self.file_handle.tell() 162 | size = self.get32() 163 | kind = self.get(4).decode("ascii") 164 | box = Box(kind) 165 | if size == 0: 166 | # signifies 'to the end of the file', we shouldn't see this. 167 | raise NotImplementedError 168 | if size == 1: 169 | # 64-bit size follows type. 170 | size = self.get64() 171 | box.size = size - 16 172 | box.after = pos + size 173 | else: 174 | box.size = size - 8 175 | box.after = pos + size 176 | box.pos = self.file_handle.tell() 177 | return box 178 | 179 | def get_full(self, box: Box) -> None: 180 | box.set_full(self.get32()) 181 | 182 | def skip(self, box: Box) -> None: 183 | self.file_handle.seek(box.after) 184 | 185 | def expect_parse(self, name: str) -> Box: 186 | while True: 187 | box = self.next_box() 188 | if box.name == name: 189 | return self.parse_box(box) 190 | self.skip(box) 191 | 192 | def get_parser(self, box: Box) -> Optional[Callable[[Box], Any]]: 193 | defs = { 194 | "ftyp": self._parse_ftyp, 195 | "meta": self._parse_meta, 196 | "infe": self._parse_infe, 197 | "iinf": self._parse_iinf, 198 | "iloc": self._parse_iloc, 199 | "hdlr": self._parse_hdlr, # HEIC/AVIF hdlr = Handler 200 | "pitm": self._parse_pitm, # HEIC/AVIF pitm = Primary Item 201 | "iref": self._parse_iref, # HEIC/AVIF idat = Item Reference 202 | "idat": self._parse_idat, # HEIC/AVIF idat = Item Data Box 203 | "dinf": self._parse_dinf, # HEIC/AVIF dinf = Data Information Box 204 | "iprp": self._parse_iprp, # HEIC/AVIF iprp = Item Protection Box 205 | } 206 | return defs.get(box.name) 207 | 208 | def parse_box(self, box: Box) -> Box: 209 | probe = self.get_parser(box) 210 | if probe is not None: 211 | probe(box) 212 | # in case anything is left unread 213 | self.file_handle.seek(box.after) 214 | return box 215 | 216 | def _parse_ftyp(self, box: Box) -> None: 217 | box.major_brand = self.get(4) 218 | box.minor_version = self.get32() 219 | box.compat = [] 220 | size = box.size - 8 221 | while size > 0: 222 | box.compat.append(self.get(4)) 223 | size -= 4 224 | 225 | def _parse_meta(self, meta: Box) -> None: 226 | self.get_full(meta) 227 | while self.file_handle.tell() < meta.after: 228 | box = self.next_box() 229 | psub = self.get_parser(box) 230 | if psub is not None: 231 | psub(box) 232 | meta.subs[box.name] = box 233 | else: 234 | logger.debug("HEIC: skipping %r", box) 235 | # skip any unparsed data 236 | self.skip(box) 237 | 238 | def _parse_infe(self, box: Box) -> None: 239 | self.get_full(box) 240 | if box.version >= 2: 241 | if box.version == 2: 242 | box.item_id = self.get16() 243 | elif box.version == 3: 244 | box.item_id = self.get32() 245 | box.item_protection_index = self.get16() 246 | box.item_type = self.get(4) 247 | box.item_name = self.get_string() 248 | # ignore the rest 249 | 250 | def _parse_iinf(self, box: Box) -> None: 251 | self.get_full(box) 252 | count = self.get16() 253 | box.exif_infe = None 254 | for _ in range(count): 255 | infe = self.expect_parse("infe") 256 | if infe.item_type == b"Exif": 257 | logger.debug("HEIC: found Exif 'infe' box") 258 | box.exif_infe = infe 259 | break 260 | 261 | def _parse_iloc(self, box: Box) -> None: 262 | self.get_full(box) 263 | size0, size1 = self.get_int4x2() 264 | size2, size3 = self.get_int4x2() 265 | box.set_sizes(size0, size1, size2, size3) 266 | if box.version < 2: 267 | box.item_count = self.get16() 268 | elif box.version == 2: 269 | box.item_count = self.get32() 270 | else: 271 | raise BoxVersion(2, box.version) 272 | box.locs = {} 273 | logger.debug("HEIC: %d iloc items", box.item_count) 274 | for _ in range(box.item_count): 275 | if box.version < 2: 276 | item_id = self.get16() 277 | elif box.version == 2: 278 | item_id = self.get32() 279 | else: 280 | # notreached 281 | raise BoxVersion(2, box.version) 282 | if box.version in (1, 2): 283 | # ignore construction_method 284 | self.get16() 285 | # ignore data_reference_index 286 | self.get16() 287 | box.base_offset = self.get_int(box.base_offset_size) 288 | extent_count = self.get16() 289 | extents = [] 290 | for _ in range(extent_count): 291 | if box.version in (1, 2) and box.index_size > 0: 292 | self.get_int(box.index_size) 293 | extent_offset = self.get_int(box.offset_size) 294 | extent_length = self.get_int(box.length_size) 295 | extents.append((extent_offset, extent_length)) 296 | box.locs[item_id] = extents 297 | 298 | # Added a few box names, which as unhandled aborted data extraction: 299 | # hdlr, pitm, dinf, iprp, idat, iref 300 | # 301 | # Handling is initially `None`. 302 | # They were found in .heif photo files produced by Nokia 8.3 5G. 303 | # 304 | # They are part of the standard, referring to: 305 | # - ISO/IEC 14496-12 fifth edition 2015-02-20 (chapter 8.10 Metadata) 306 | # found in: 307 | # https://mpeg.chiariglione.org/standards/mpeg-4/iso-base-media-file-format/text-isoiec-14496-12-5th-edition 308 | # (The newest is ISO/IEC 14496-12:2022, but would cost 208 Swiss Francs at iso.org) 309 | # - A C++ example: https://exiv2.org/book/#BMFF 310 | 311 | def _parse_hdlr(self, box: Box) -> None: 312 | logger.debug("HEIC: found 'hdlr' Box %s, skipped", box.name) 313 | 314 | def _parse_pitm(self, box: Box) -> None: 315 | logger.debug("HEIC: found 'pitm' Box %s, skipped", box.name) 316 | 317 | def _parse_dinf(self, box: Box) -> None: 318 | logger.debug("HEIC: found 'dinf' Box %s, skipped", box.name) 319 | 320 | def _parse_iprp(self, box: Box) -> None: 321 | logger.debug("HEIC: found 'iprp' Box %s, skipped", box.name) 322 | 323 | def _parse_idat(self, box: Box) -> None: 324 | logger.debug("HEIC: found 'idat' Box %s, skipped", box.name) 325 | 326 | def _parse_iref(self, box: Box) -> None: 327 | logger.debug("HEIC: found 'iref' Box %s, skipped", box.name) 328 | 329 | def find_exif(self) -> Tuple[int, bytes]: 330 | ftyp = self.expect_parse("ftyp") 331 | if ( 332 | ftyp.major_brand not in [b"heic", b"avif", b"mif1"] 333 | or ftyp.minor_version != 0 334 | ): 335 | return 0, b"" 336 | 337 | meta = self.expect_parse("meta") 338 | if meta.subs["iinf"].exif_infe is None: 339 | return 0, b"" 340 | 341 | item_id = meta.subs["iinf"].exif_infe.item_id 342 | extents = meta.subs["iloc"].locs[item_id] 343 | logger.debug("HEIC: found Exif location.") 344 | # we expect the Exif data to be in one piece. 345 | assert len(extents) == 1 346 | pos, _ = extents[0] 347 | # looks like there's a kind of pseudo-box here. 348 | self.file_handle.seek(pos) 349 | # the payload of "Exif" item may be start with either 350 | # b'\xFF\xE1\xSS\xSSExif\x00\x00' (with APP1 marker, e.g. Android Q) 351 | # or 352 | # b'Exif\x00\x00' (without APP1 marker, e.g. iOS) 353 | # according to "ISO/IEC 23008-12, 2017-12", both of them are legal 354 | exif_tiff_header_offset = self.get32() 355 | 356 | if exif_tiff_header_offset == 0: 357 | # This case was found in HMD Nokia 8.3 5G heic photos. 358 | # The TIFF header just sits there without any 'Exif'. 359 | 360 | offset = 0 361 | endian = b"?" # Haven't got Endian info yet 362 | else: 363 | assert exif_tiff_header_offset >= 6 364 | assert self.get(exif_tiff_header_offset)[-6:] == b"Exif\x00\x00" 365 | offset = self.file_handle.tell() 366 | endian = self.file_handle.read(1) 367 | 368 | return offset, endian 369 | -------------------------------------------------------------------------------- /exifread/core/ifd_tag.py: -------------------------------------------------------------------------------- 1 | """ 2 | Eases dealing with tags. 3 | """ 4 | 5 | from exifread.tags.fields import FIELD_DEFINITIONS, FieldType 6 | 7 | 8 | class IfdTag: 9 | """ 10 | Represents an IFD tag. 11 | """ 12 | 13 | def __init__( 14 | self, 15 | printable: str, 16 | tag: int, 17 | field_type: FieldType, 18 | values, 19 | field_offset: int, 20 | field_length: int, 21 | prefer_printable: bool = True, 22 | ) -> None: 23 | # printable version of data 24 | self.printable = printable 25 | # tag ID number 26 | self.tag = tag 27 | # field type as index into FIELD_TYPES 28 | self.field_type = field_type 29 | # offset of start of field in bytes from beginning of IFD 30 | self.field_offset = field_offset 31 | # length of data field in bytes 32 | self.field_length = field_length 33 | # either string, bytes or list of data items 34 | # TODO: sort out this type mess! 35 | self.values = values 36 | # indication if printable version should be used upon serialization 37 | self.prefer_printable = prefer_printable 38 | 39 | def __str__(self) -> str: 40 | return self.printable 41 | 42 | def __repr__(self) -> str: 43 | try: 44 | tag = "(0x%04X) %s=%s @ %d" % ( 45 | self.tag, 46 | FIELD_DEFINITIONS[self.field_type][1], 47 | self.printable, 48 | self.field_offset, 49 | ) 50 | except TypeError: 51 | tag = "(%s) %s=%s @ %s" % ( 52 | str(self.tag), 53 | FIELD_DEFINITIONS[self.field_type][1], 54 | self.printable, 55 | str(self.field_offset), 56 | ) 57 | return tag 58 | -------------------------------------------------------------------------------- /exifread/core/jpeg.py: -------------------------------------------------------------------------------- 1 | """Extract EXIF from JPEG files.""" 2 | 3 | from typing import BinaryIO, Tuple 4 | 5 | from exifread.core.exceptions import InvalidExif 6 | from exifread.core.utils import ord_ 7 | from exifread.exif_log import get_logger 8 | 9 | logger = get_logger() 10 | 11 | 12 | def _increment_base(data, base) -> int: 13 | return ord_(data[base + 2]) * 256 + ord_(data[base + 3]) + 2 14 | 15 | 16 | def _get_initial_base(fh: BinaryIO, data: bytes, fake_exif: int) -> Tuple[int, int]: 17 | base = 2 18 | logger.debug( 19 | "data[2]=0x%X data[3]=0x%X data[6:10]=%s", 20 | ord_(data[2]), 21 | ord_(data[3]), 22 | data[6:10], 23 | ) 24 | while ord_(data[2]) == 0xFF and data[6:10] in (b"JFIF", b"JFXX", b"OLYM", b"Phot"): 25 | length = ord_(data[4]) * 256 + ord_(data[5]) 26 | logger.debug(" Length offset is %s", length) 27 | fh.read(length - 8) 28 | # fake an EXIF beginning of file 29 | # I don't think this is used. --gd 30 | data = b"\xff\x00" + fh.read(10) 31 | fake_exif = 1 32 | if base > 2: 33 | logger.debug(" Added to base") 34 | base = base + length + 4 - 2 35 | else: 36 | logger.debug(" Added to zero") 37 | base = length + 4 38 | logger.debug(" Set segment base to 0x%X", base) 39 | return base, fake_exif 40 | 41 | 42 | def _get_base(base: int, data: bytes) -> int: 43 | # pylint: disable=too-many-statements 44 | while True: 45 | logger.debug(" Segment base 0x%X", base) 46 | if data[base : base + 2] == b"\xff\xe1": 47 | # APP1 48 | logger.debug(" APP1 at base 0x%X", base) 49 | logger.debug( 50 | " Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3]) 51 | ) 52 | logger.debug(" Code: %s", data[base + 4 : base + 8]) 53 | if data[base + 4 : base + 8] == b"Exif": 54 | logger.debug( 55 | " Decrement base by 2 to get to pre-segment header (for compatibility with later code)" 56 | ) 57 | base -= 2 58 | break 59 | increment = _increment_base(data, base) 60 | logger.debug(" Increment base by %s", increment) 61 | base += increment 62 | elif data[base : base + 2] == b"\xff\xe0": 63 | # APP0 64 | logger.debug(" APP0 at base 0x%X", base) 65 | logger.debug( 66 | " Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3]) 67 | ) 68 | logger.debug(" Code: %s", data[base + 4 : base + 8]) 69 | increment = _increment_base(data, base) 70 | logger.debug(" Increment base by %s", increment) 71 | base += increment 72 | elif data[base : base + 2] == b"\xff\xe2": 73 | # APP2 74 | logger.debug(" APP2 at base 0x%X", base) 75 | logger.debug( 76 | " Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3]) 77 | ) 78 | logger.debug(" Code: %s", data[base + 4 : base + 8]) 79 | increment = _increment_base(data, base) 80 | logger.debug(" Increment base by %s", increment) 81 | base += increment 82 | elif data[base : base + 2] == b"\xff\xee": 83 | # APP14 84 | logger.debug(" APP14 Adobe segment at base 0x%X", base) 85 | logger.debug( 86 | " Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3]) 87 | ) 88 | logger.debug(" Code: %s", data[base + 4 : base + 8]) 89 | increment = _increment_base(data, base) 90 | logger.debug(" Increment base by %s", increment) 91 | base += increment 92 | logger.debug( 93 | " There is useful EXIF-like data here, but we have no parser for it." 94 | ) 95 | elif data[base : base + 2] == b"\xff\xdb": 96 | logger.debug( 97 | " JPEG image data at base 0x%X No more segments are expected.", base 98 | ) 99 | break 100 | elif data[base : base + 2] == b"\xff\xd8": 101 | # APP12 102 | logger.debug(" FFD8 segment at base 0x%X", base) 103 | logger.debug( 104 | " Got 0x%X 0x%X and %s instead", 105 | ord_(data[base]), 106 | ord_(data[base + 1]), 107 | data[4 + base : 10 + base], 108 | ) 109 | logger.debug( 110 | " Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3]) 111 | ) 112 | logger.debug(" Code: %s", data[base + 4 : base + 8]) 113 | increment = _increment_base(data, base) 114 | logger.debug(" Increment base by %s", increment) 115 | base += increment 116 | elif data[base : base + 2] == b"\xff\xec": 117 | # APP12 118 | logger.debug( 119 | " APP12 XMP (Ducky) or Pictureinfo segment at base 0x%X", base 120 | ) 121 | logger.debug( 122 | " Got 0x%X and 0x%X instead", ord_(data[base]), ord_(data[base + 1]) 123 | ) 124 | logger.debug( 125 | " Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3]) 126 | ) 127 | logger.debug("Code: %s", data[base + 4 : base + 8]) 128 | increment = _increment_base(data, base) 129 | logger.debug(" Increment base by %s", increment) 130 | base += increment 131 | logger.debug( 132 | ( 133 | " There is useful EXIF-like data here (quality, comment, copyright), " 134 | "but we have no parser for it." 135 | ) 136 | ) 137 | else: 138 | try: 139 | increment = _increment_base(data, base) 140 | logger.debug( 141 | " Got 0x%X and 0x%X instead", 142 | ord_(data[base]), 143 | ord_(data[base + 1]), 144 | ) 145 | except IndexError as err: 146 | raise InvalidExif( 147 | "Unexpected/unhandled segment type or file content." 148 | ) from err 149 | logger.debug(" Increment base by %s", increment) 150 | base += increment 151 | return base 152 | 153 | 154 | def find_jpeg_exif(fh: BinaryIO, data: bytes, fake_exif: int) -> Tuple[int, bytes, int]: 155 | logger.debug( 156 | "JPEG format recognized data[0:2]=0x%X%X", ord_(data[0]), ord_(data[1]) 157 | ) 158 | 159 | base, fake_exif = _get_initial_base(fh, data, fake_exif) 160 | 161 | # Big ugly patch to deal with APP2 (or other) data coming before APP1 162 | fh.seek(0) 163 | # in theory, this could be insufficient since 64K is the maximum size--gd 164 | data = fh.read(base + 4000) 165 | 166 | base = _get_base(base, data) 167 | 168 | fh.seek(base + 12) 169 | if ord_(data[2 + base]) == 0xFF and data[6 + base : 10 + base] == b"Exif": 170 | # detected EXIF header 171 | offset = fh.tell() 172 | endian = fh.read(1) 173 | # HACK TEST: endian = 'M' 174 | elif ord_(data[2 + base]) == 0xFF and data[6 + base : 10 + base + 1] == b"Ducky": 175 | # detected Ducky header. 176 | logger.debug( 177 | "EXIF-like header (normally 0xFF and code): 0x%X and %s", 178 | ord_(data[2 + base]), 179 | data[6 + base : 10 + base + 1], 180 | ) 181 | offset = fh.tell() 182 | endian = fh.read(1) 183 | elif ord_(data[2 + base]) == 0xFF and data[6 + base : 10 + base + 1] == b"Adobe": 184 | # detected APP14 (Adobe) 185 | logger.debug( 186 | "EXIF-like header (normally 0xFF and code): 0x%X and %s", 187 | ord_(data[2 + base]), 188 | data[6 + base : 10 + base + 1], 189 | ) 190 | offset = fh.tell() 191 | endian = fh.read(1) 192 | else: 193 | # no EXIF information 194 | msg = "No EXIF header expected data[2+base]==0xFF and data[6+base:10+base]===Exif (or Duck)" 195 | msg += " Did get 0x%X and %r" % ( 196 | ord_(data[2 + base]), 197 | data[6 + base : 10 + base + 1], 198 | ) 199 | raise InvalidExif(msg) 200 | return offset, endian, fake_exif 201 | -------------------------------------------------------------------------------- /exifread/core/jxl.py: -------------------------------------------------------------------------------- 1 | """ 2 | Find Exif data in a JPEG XL file 3 | """ 4 | 5 | from typing import Tuple 6 | 7 | from exifread.core.heic import HEICExifFinder 8 | 9 | 10 | class JXLExifFinder(HEICExifFinder): 11 | """Find JPEG XL EXIF tags.""" 12 | 13 | def find_exif(self) -> Tuple[int, bytes]: 14 | ftyp = self.expect_parse("ftyp") 15 | assert ftyp.major_brand == b"jxl " 16 | assert ftyp.minor_version == 0 17 | exif = self.expect_parse("Exif") 18 | 19 | offset = exif.pos + 4 20 | self.file_handle.seek(offset - 8) 21 | assert self.get(8)[:6] == b"Exif\x00\x00" 22 | endian = self.file_handle.read(1) 23 | return offset, endian 24 | -------------------------------------------------------------------------------- /exifread/core/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Misc utilities. 3 | """ 4 | 5 | from typing import Union 6 | 7 | 8 | def ord_(dta: Union[str, int]) -> int: 9 | if isinstance(dta, str): 10 | return ord(dta) 11 | return dta 12 | -------------------------------------------------------------------------------- /exifread/core/xmp.py: -------------------------------------------------------------------------------- 1 | """XMP related utilities..""" 2 | 3 | from pyexpat import ExpatError 4 | from typing import BinaryIO 5 | from xml.dom.minidom import parseString 6 | 7 | from exifread.exif_log import get_logger 8 | 9 | logger = get_logger() 10 | 11 | 12 | def find_xmp_data(fh: BinaryIO) -> bytes: 13 | xmp_bytes = b"" 14 | logger.debug("XMP not in Exif, searching file for XMP info...") 15 | xml_started = False 16 | xml_finished = False 17 | for line in fh: 18 | open_tag = line.find(b"") 20 | if open_tag != -1: 21 | xml_started = True 22 | line = line[open_tag:] 23 | logger.debug("XMP found opening tag at line position %s", open_tag) 24 | if close_tag != -1: 25 | logger.debug("XMP found closing tag at line position %s", close_tag) 26 | line_offset = 0 27 | if open_tag != -1: 28 | line_offset = open_tag 29 | line = line[: (close_tag - line_offset) + 12] 30 | xml_finished = True 31 | if xml_started: 32 | xmp_bytes += line 33 | if xml_finished: 34 | break 35 | logger.debug("Found %s XMP bytes", len(xmp_bytes)) 36 | return xmp_bytes 37 | 38 | 39 | def xmp_bytes_to_str(xmp_bytes: bytes) -> str: 40 | """Adobe's Extensible Metadata Platform, just dump the pretty XML.""" 41 | 42 | logger.debug("Cleaning XMP data ...") 43 | 44 | # Pray that it's encoded in UTF-8 45 | # TODO: allow user to specify encoding 46 | xmp_string = xmp_bytes.decode("utf-8") 47 | 48 | try: 49 | pretty = parseString(xmp_string).toprettyxml() 50 | except ExpatError: 51 | logger.warning("XMP: XML is not well formed") 52 | return xmp_string 53 | cleaned = [] 54 | for line in pretty.splitlines(): 55 | if line.strip(): 56 | cleaned.append(line) 57 | return "\n".join(cleaned) 58 | -------------------------------------------------------------------------------- /exifread/exif_log.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom log output. 3 | """ 4 | 5 | import logging 6 | import sys 7 | 8 | TEXT_NORMAL = 0 9 | TEXT_BOLD = 1 10 | TEXT_RED = 31 11 | TEXT_GREEN = 32 12 | TEXT_YELLOW = 33 13 | TEXT_BLUE = 34 14 | TEXT_MAGENTA = 35 15 | TEXT_CYAN = 36 16 | 17 | 18 | def get_logger() -> logging.Logger: 19 | """Use this from all files needing to log.""" 20 | return logging.getLogger("exifread") 21 | 22 | 23 | def setup_logger(debug: bool, color: bool) -> None: 24 | """Configure the logger.""" 25 | 26 | if debug: 27 | log_level = logging.DEBUG 28 | else: 29 | log_level = logging.INFO 30 | 31 | logger = get_logger() 32 | stream = Handler(log_level, debug, color) 33 | logger.addHandler(stream) 34 | logger.setLevel(log_level) 35 | 36 | 37 | class Formatter(logging.Formatter): 38 | """ 39 | Custom formatter, we like colors! 40 | """ 41 | 42 | color: bool 43 | debug: bool 44 | 45 | def __init__(self, debug: bool = False, color: bool = False) -> None: 46 | self.color = color 47 | self.debug = debug 48 | if self.debug: 49 | log_format = "%(levelname)-6s %(message)s" 50 | else: 51 | log_format = "%(message)s" 52 | logging.Formatter.__init__(self, log_format) 53 | 54 | def format(self, record): 55 | if self.debug and self.color: 56 | if record.levelno >= logging.CRITICAL: 57 | color = TEXT_RED 58 | elif record.levelno >= logging.ERROR: 59 | color = TEXT_RED 60 | elif record.levelno >= logging.WARNING: 61 | color = TEXT_YELLOW 62 | elif record.levelno >= logging.INFO: 63 | color = TEXT_GREEN 64 | elif record.levelno >= logging.DEBUG: 65 | color = TEXT_CYAN 66 | else: 67 | color = TEXT_NORMAL 68 | record.levelname = "\x1b[%sm%s\x1b[%sm" % ( 69 | color, 70 | record.levelname, 71 | TEXT_NORMAL, 72 | ) 73 | return logging.Formatter.format(self, record) 74 | 75 | 76 | class Handler(logging.StreamHandler): 77 | """ 78 | Custom StreamHandler so we can use the Formatter. 79 | """ 80 | 81 | color: bool 82 | debug: bool 83 | 84 | def __init__( 85 | self, log_level: int, debug: bool = False, color: bool = False 86 | ) -> None: 87 | self.color = color 88 | self.debug = debug 89 | logging.StreamHandler.__init__(self, sys.stdout) 90 | self.setFormatter(Formatter(debug, color)) 91 | self.setLevel(log_level) 92 | 93 | 94 | # def emit(self, record): 95 | # record.msg = "\x1b[%sm%s\x1b[%sm" % (TEXT_BOLD, record.msg, TEXT_NORMAL) 96 | # logging.StreamHandler.emit(self, record) 97 | -------------------------------------------------------------------------------- /exifread/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/exifread/py.typed -------------------------------------------------------------------------------- /exifread/serialize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Enable conversion of Exif IfdTags to native Python types 3 | """ 4 | 5 | from typing import Callable, Dict, List, Union 6 | 7 | from exifread.core.exif_header import IfdTag 8 | from exifread.exif_log import get_logger 9 | from exifread.tags.fields import FieldType 10 | 11 | logger = get_logger() 12 | 13 | SerializedTagValue = Union[int, float, str, bytes, List[int], List[float], None] 14 | SerializedTagDict = Dict[str, SerializedTagValue] 15 | 16 | 17 | def convert_types( 18 | exif_tags: Dict[str, Union[IfdTag, bytes]], 19 | ) -> SerializedTagDict: 20 | """ 21 | Convert Exif IfdTags to built-in Python types for easier serialization and programmatic use. 22 | 23 | - If the printable value of the IfdTag is relevant (e.g. enum type), it is preserved. 24 | - Otherwise, values are processed based on their field type, with some cleanups applied. 25 | - Single-element lists are unpacked to return the item directly. 26 | """ 27 | 28 | output: SerializedTagDict = {} 29 | 30 | for tag_name, ifd_tag in exif_tags.items(): 31 | # JPEGThumbnail and TIFFThumbnail are the only values 32 | # in Exif Tags dict that do not have the IfdTag type. 33 | if isinstance(ifd_tag, bytes): 34 | output[tag_name] = ifd_tag 35 | continue 36 | 37 | convert_func: Callable[[IfdTag, str], SerializedTagValue] 38 | 39 | if ifd_tag.prefer_printable: 40 | # Prioritize the printable value if prefer_printable is set 41 | convert_func = convert_proprietary 42 | 43 | else: 44 | # Get the conversion function based on field type 45 | try: 46 | convert_func = conversion_map[ifd_tag.field_type] 47 | except KeyError: 48 | logger.error( 49 | "Type conversion for field type %s not explicitly supported", 50 | ifd_tag.field_type, 51 | ) 52 | convert_func = convert_proprietary # Fallback to printable 53 | 54 | output[tag_name] = convert_func(ifd_tag, tag_name) 55 | 56 | # Useful only for testing 57 | # logger.warning( 58 | # f"{convert_func.__name__}: {ifd_tag.field_type} to {type(output[tag_name]).__name__}\n" 59 | # f"{tag_name} --> {str(output[tag_name])[:30]!r}" 60 | # ) 61 | 62 | return output 63 | 64 | 65 | def convert_ascii(ifd_tag: IfdTag, tag_name: str) -> Union[str, bytes, None]: 66 | """ 67 | Handle ASCII conversion, including special date formats. 68 | 69 | Returns: 70 | - str 71 | - bytes for rare ascii sequences that aren't Unicode 72 | - None for empty values 73 | """ 74 | 75 | out = ifd_tag.values 76 | 77 | # Handle DateTime formatting; often formatted in a way that cannot 78 | # be parsed by Python dateutil (%Y:%m:%d %H:%M:%S). 79 | if "DateTime" in tag_name and len(out) == 19 and out.count(":") == 4: 80 | out = out.replace(":", "-", 2) 81 | 82 | # Handle GPSDate formatting; these are proper dates with the wrong 83 | # delimiter (':' rather than '-'). Invalid values have been found 84 | # in test images: '' and '2014:09:259' 85 | elif tag_name == "GPS GPSDate" and len(out) == 10 and out.count(":") == 2: 86 | out = out.replace(":", "-") 87 | 88 | # Strip occasional trailing whitespaces 89 | out = out.strip() 90 | 91 | if not out: 92 | return None 93 | 94 | # Attempt to decode bytes if unicode 95 | if isinstance(out, bytes): 96 | try: 97 | return out.decode() 98 | except UnicodeDecodeError: 99 | pass 100 | 101 | return out 102 | 103 | 104 | def convert_undefined(ifd_tag: IfdTag, _tag_name: str) -> Union[bytes, str, int, None]: 105 | """ 106 | Handle Undefined type conversion. 107 | 108 | Returns: 109 | - bytes if not Unicode such as Exif MakerNote 110 | - str for Unicode 111 | - int for rare MakerNote Tags containing a single value 112 | - None for empty values such as some MakerNote Tags 113 | """ 114 | 115 | out = ifd_tag.values 116 | 117 | if len(out) == 1: 118 | # Return integer from single-element list 119 | return out[0] 120 | 121 | # These contain bytes represented as a list of integers, sometimes with surrounded by spaces and/or null bytes 122 | out = bytes(out).strip(b" \x00") 123 | 124 | if not out: 125 | return None 126 | 127 | # Empty byte sequences or Unicode values should be decoded as strings 128 | try: 129 | return out.decode() 130 | except UnicodeDecodeError: 131 | return out 132 | 133 | 134 | def convert_numeric(ifd_tag: IfdTag, _tag_name: str) -> Union[int, List[int], None]: 135 | """ 136 | Handle numeric types conversion. 137 | 138 | Returns: 139 | - int in most cases 140 | - list of int 141 | - None for empty values such as some MakerNote Tags 142 | 143 | Note: All Floating Point tags seen were empty. 144 | """ 145 | 146 | out = ifd_tag.values 147 | 148 | if not out: # Empty lists, seen in floating point numbers 149 | return None 150 | 151 | return out[0] if len(out) == 1 else out 152 | 153 | 154 | def convert_ratio( 155 | ifd_tag: IfdTag, _tag_name: str 156 | ) -> Union[int, float, List[int], List[float], None]: 157 | """ 158 | Handle Ratio and Signed Ratio conversion. 159 | 160 | Returns: 161 | - int when the denominator is 1 or unused 162 | - float otherwise 163 | - a list of int or float, such as GPS Latitude/Longitude/TimeStamp 164 | - None for empty values such as some MakerNote Tags 165 | 166 | Ratios can be re-created with `Ratio(float_value).limit_denominator()`. 167 | """ 168 | 169 | out = [] 170 | 171 | for ratio in ifd_tag.values: 172 | # Prevent division by 0. Sometimes, EXIF data is full of 0s when a feature is unused. 173 | if ratio.denominator == 0: 174 | ratio = ratio.numerator 175 | 176 | ratio = float(ratio) 177 | 178 | if ratio.is_integer(): 179 | ratio = int(ratio) 180 | 181 | out.append(ratio) 182 | 183 | if not out: 184 | return None 185 | 186 | return out[0] if len(out) == 1 else out 187 | 188 | 189 | def convert_bytes(ifd_tag: IfdTag, tag_name: str) -> Union[bytes, str, int, None]: 190 | """ 191 | Handle Byte and Signed Byte conversion. 192 | 193 | Returns: 194 | - bytes 195 | - str for Unicode such as GPSVersionID and Image ApplicationNotes (XML) 196 | - int for single byte values such as GPSAltitudeRef or some MakerNote fields 197 | - None for empty values such as some MakerNote Tags 198 | """ 199 | 200 | out = ifd_tag.values 201 | 202 | if len(out) == 1: 203 | # Byte can be a single integer, such as GPSAltitudeRef (0 or 1) 204 | return out[0] 205 | 206 | if tag_name == "GPS GPSVersionID": 207 | return ".".join(map(str, out)) # e.g. [2, 3, 0, 0] --> '2.3.0.0' 208 | 209 | # Byte sequences are often surrounded by or only composed of spaces and/or null bytes 210 | out = bytes(out).strip(b" \x00") 211 | 212 | if not out: 213 | return None 214 | 215 | # Unicode values should be decoded as strings (e.g. XML) 216 | try: 217 | return out.decode() 218 | except UnicodeDecodeError: 219 | return out 220 | 221 | 222 | def convert_proprietary(ifd_tag: IfdTag, _tag_name: str) -> Union[str, None]: 223 | """ 224 | Handle Proprietary type conversion. 225 | 226 | Returns: 227 | - str as all tags of this made-up type (e.g. enums) prefer printable 228 | - None for very rare empty printable values 229 | """ 230 | 231 | out = ifd_tag.printable 232 | if not out or out == "[]": 233 | return None 234 | 235 | return out 236 | 237 | 238 | # Mapping of field type to conversion function 239 | conversion_map: Dict[FieldType, Callable] = { 240 | FieldType.PROPRIETARY: convert_proprietary, 241 | FieldType.BYTE: convert_bytes, 242 | FieldType.ASCII: convert_ascii, 243 | FieldType.SHORT: convert_numeric, 244 | FieldType.LONG: convert_numeric, 245 | FieldType.RATIO: convert_ratio, 246 | FieldType.SIGNED_BYTE: convert_numeric, 247 | FieldType.UNDEFINED: convert_undefined, 248 | FieldType.SIGNED_SHORT: convert_numeric, 249 | FieldType.SIGNED_LONG: convert_numeric, 250 | FieldType.SIGNED_RATIO: convert_ratio, 251 | FieldType.FLOAT_32: convert_numeric, 252 | FieldType.FLOAT_64: convert_numeric, 253 | FieldType.IFD: convert_bytes, 254 | } 255 | -------------------------------------------------------------------------------- /exifread/tags/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tag definitions 3 | """ 4 | 5 | from typing import Callable, Dict, List, Tuple, Union 6 | 7 | DEFAULT_STOP_TAG = "UNDEF" 8 | 9 | 10 | # To ignore when quick processing 11 | IGNORE_TAGS: List[int] = [ 12 | 0x02BC, # XPM 13 | 0x927C, # MakerNote Tags 14 | 0x9286, # user comment 15 | ] 16 | 17 | SubIfdTagDictValue = Tuple[str, Union[dict, Callable, None]] 18 | SubIfdTagDict = Dict[int, SubIfdTagDictValue] 19 | SubIfdInfoTuple = Tuple[str, SubIfdTagDict] 20 | 21 | IfdDictValue = Union[Tuple[str, SubIfdInfoTuple], SubIfdTagDictValue] 22 | IfdTagDict = Dict[int, IfdDictValue] 23 | -------------------------------------------------------------------------------- /exifread/tags/exif.py: -------------------------------------------------------------------------------- 1 | """ 2 | Standard tag definitions. 3 | """ 4 | 5 | from exifread.tags import IfdTagDict, SubIfdInfoTuple, SubIfdTagDict 6 | from exifread.tags.str_utils import make_string, make_string_uc 7 | 8 | # Interoperability tags 9 | INTEROP_TAGS: SubIfdTagDict = { 10 | 0x0001: ("InteroperabilityIndex", None), 11 | 0x0002: ("InteroperabilityVersion", None), 12 | 0x1000: ("RelatedImageFileFormat", None), 13 | 0x1001: ("RelatedImageWidth", None), 14 | 0x1002: ("RelatedImageLength", None), 15 | } 16 | INTEROP_INFO: SubIfdInfoTuple = ("Interoperability", INTEROP_TAGS) 17 | 18 | # GPS tags 19 | GPS_TAGS: SubIfdTagDict = { 20 | 0x0000: ("GPSVersionID", None), 21 | 0x0001: ("GPSLatitudeRef", None), 22 | 0x0002: ("GPSLatitude", None), 23 | 0x0003: ("GPSLongitudeRef", None), 24 | 0x0004: ("GPSLongitude", None), 25 | 0x0005: ("GPSAltitudeRef", None), 26 | 0x0006: ("GPSAltitude", None), 27 | 0x0007: ("GPSTimeStamp", None), 28 | 0x0008: ("GPSSatellites", None), 29 | 0x0009: ("GPSStatus", None), 30 | 0x000A: ("GPSMeasureMode", None), 31 | 0x000B: ("GPSDOP", None), 32 | 0x000C: ("GPSSpeedRef", None), 33 | 0x000D: ("GPSSpeed", None), 34 | 0x000E: ("GPSTrackRef", None), 35 | 0x000F: ("GPSTrack", None), 36 | 0x0010: ("GPSImgDirectionRef", None), 37 | 0x0011: ("GPSImgDirection", None), 38 | 0x0012: ("GPSMapDatum", None), 39 | 0x0013: ("GPSDestLatitudeRef", None), 40 | 0x0014: ("GPSDestLatitude", None), 41 | 0x0015: ("GPSDestLongitudeRef", None), 42 | 0x0016: ("GPSDestLongitude", None), 43 | 0x0017: ("GPSDestBearingRef", None), 44 | 0x0018: ("GPSDestBearing", None), 45 | 0x0019: ("GPSDestDistanceRef", None), 46 | 0x001A: ("GPSDestDistance", None), 47 | 0x001B: ("GPSProcessingMethod", None), 48 | 0x001C: ("GPSAreaInformation", None), 49 | 0x001D: ("GPSDate", None), 50 | 0x001E: ("GPSDifferential", None), 51 | } 52 | GPS_INFO: SubIfdInfoTuple = ("GPS", GPS_TAGS) 53 | 54 | # Main Exif tag names 55 | EXIF_TAGS: IfdTagDict = { 56 | 0x00FE: ( 57 | "SubfileType", 58 | { 59 | 0x0: "Full-resolution Image", 60 | 0x1: "Reduced-resolution image", 61 | 0x2: "Single page of multi-page image", 62 | 0x3: "Single page of multi-page reduced-resolution image", 63 | 0x4: "Transparency mask", 64 | 0x5: "Transparency mask of reduced-resolution image", 65 | 0x6: "Transparency mask of multi-page image", 66 | 0x7: "Transparency mask of reduced-resolution multi-page image", 67 | 0x10001: "Alternate reduced-resolution image", 68 | 0xFFFFFFFF: "invalid ", 69 | }, 70 | ), 71 | 0x00FF: ( 72 | "OldSubfileType", 73 | { 74 | 1: "Full-resolution image", 75 | 2: "Reduced-resolution image", 76 | 3: "Single page of multi-page image", 77 | }, 78 | ), 79 | 0x0100: ("ImageWidth", None), 80 | 0x0101: ("ImageLength", None), 81 | 0x0102: ("BitsPerSample", None), 82 | 0x0103: ( 83 | "Compression", 84 | { 85 | 1: "Uncompressed", 86 | 2: "CCITT 1D", 87 | 3: "T4/Group 3 Fax", 88 | 4: "T6/Group 4 Fax", 89 | 5: "LZW", 90 | 6: "JPEG (old-style)", 91 | 7: "JPEG", 92 | 8: "Adobe Deflate", 93 | 9: "JBIG B&W", 94 | 10: "JBIG Color", 95 | 32766: "Next", 96 | 32769: "Epson ERF Compressed", 97 | 32771: "CCIRLEW", 98 | 32773: "PackBits", 99 | 32809: "Thunderscan", 100 | 32895: "IT8CTPAD", 101 | 32896: "IT8LW", 102 | 32897: "IT8MP", 103 | 32898: "IT8BL", 104 | 32908: "PixarFilm", 105 | 32909: "PixarLog", 106 | 32946: "Deflate", 107 | 32947: "DCS", 108 | 34661: "JBIG", 109 | 34676: "SGILog", 110 | 34677: "SGILog24", 111 | 34712: "JPEG 2000", 112 | 34713: "Nikon NEF Compressed", 113 | 65000: "Kodak DCR Compressed", 114 | 65535: "Pentax PEF Compressed", 115 | }, 116 | ), 117 | 0x0106: ("PhotometricInterpretation", None), 118 | 0x0107: ("Thresholding", None), 119 | 0x0108: ("CellWidth", None), 120 | 0x0109: ("CellLength", None), 121 | 0x010A: ("FillOrder", None), 122 | 0x010D: ("DocumentName", None), 123 | 0x010E: ("ImageDescription", None), 124 | 0x010F: ("Make", None), 125 | 0x0110: ("Model", None), 126 | 0x0111: ("StripOffsets", None), 127 | 0x0112: ( 128 | "Orientation", 129 | { 130 | 1: "Horizontal (normal)", 131 | 2: "Mirrored horizontal", 132 | 3: "Rotated 180", 133 | 4: "Mirrored vertical", 134 | 5: "Mirrored horizontal then rotated 90 CCW", 135 | 6: "Rotated 90 CW", 136 | 7: "Mirrored horizontal then rotated 90 CW", 137 | 8: "Rotated 90 CCW", 138 | }, 139 | ), 140 | 0x0115: ("SamplesPerPixel", None), 141 | 0x0116: ("RowsPerStrip", None), 142 | 0x0117: ("StripByteCounts", None), 143 | 0x0118: ("MinSampleValue", None), 144 | 0x0119: ("MaxSampleValue", None), 145 | 0x011A: ("XResolution", None), 146 | 0x011B: ("YResolution", None), 147 | 0x011C: ("PlanarConfiguration", None), 148 | 0x011D: ("PageName", make_string), 149 | 0x011E: ("XPosition", None), 150 | 0x011F: ("YPosition", None), 151 | 0x0122: ( 152 | "GrayResponseUnit", 153 | { 154 | 1: "0.1", 155 | 2: "0.001", 156 | 3: "0.0001", 157 | 4: "1e-05", 158 | 5: "1e-06", 159 | }, 160 | ), 161 | 0x0123: ("GrayResponseCurve", None), 162 | 0x0124: ("T4Options", None), 163 | 0x0125: ("T6Options", None), 164 | 0x0128: ( 165 | "ResolutionUnit", 166 | {1: "Not Absolute", 2: "Pixels/Inch", 3: "Pixels/Centimeter"}, 167 | ), 168 | 0x0129: ("PageNumber", None), 169 | 0x012C: ("ColorResponseUnit", None), 170 | 0x012D: ("TransferFunction", None), 171 | 0x0131: ("Software", None), 172 | 0x0132: ("DateTime", None), 173 | 0x013B: ("Artist", None), 174 | 0x013C: ("HostComputer", None), 175 | 0x013D: ("Predictor", {1: "None", 2: "Horizontal differencing"}), 176 | 0x013E: ("WhitePoint", None), 177 | 0x013F: ("PrimaryChromaticities", None), 178 | 0x0140: ("ColorMap", None), 179 | 0x0141: ("HalftoneHints", None), 180 | 0x0142: ("TileWidth", None), 181 | 0x0143: ("TileLength", None), 182 | 0x0144: ("TileOffsets", None), 183 | 0x0145: ("TileByteCounts", None), 184 | 0x0146: ("BadFaxLines", None), 185 | 0x0147: ("CleanFaxData", {0: "Clean", 1: "Regenerated", 2: "Unclean"}), 186 | 0x014A: ("SubIFDs", None), 187 | 0x0148: ("ConsecutiveBadFaxLines", None), 188 | 0x014C: ("InkSet", {1: "CMYK", 2: "Not CMYK"}), 189 | 0x014D: ("InkNames", None), 190 | 0x014E: ("NumberofInks", None), 191 | 0x0150: ("DotRange", None), 192 | 0x0151: ("TargetPrinter", None), 193 | 0x0152: ( 194 | "ExtraSamples", 195 | {0: "Unspecified", 1: "Associated Alpha", 2: "Unassociated Alpha"}, 196 | ), 197 | 0x0153: ( 198 | "SampleFormat", 199 | { 200 | 1: "Unsigned", 201 | 2: "Signed", 202 | 3: "Float", 203 | 4: "Undefined", 204 | 5: "Complex int", 205 | 6: "Complex float", 206 | }, 207 | ), 208 | 0x0154: ("SMinSampleValue", None), 209 | 0x0155: ("SMaxSampleValue", None), 210 | 0x0156: ("TransferRange", None), 211 | 0x0157: ("ClipPath", None), 212 | 0x015B: ("JPEGTables", None), 213 | 0x0200: ("JPEGProc", None), 214 | 0x0201: ("JPEGInterchangeFormat", None), # JpegIFOffset 215 | 0x0202: ("JPEGInterchangeFormatLength", None), # JpegIFByteCount 216 | 0x0211: ("YCbCrCoefficients", None), 217 | 0x0212: ("YCbCrSubSampling", None), 218 | 0x0213: ("YCbCrPositioning", {1: "Centered", 2: "Co-sited"}), 219 | 0x0214: ("ReferenceBlackWhite", None), 220 | 0x02BC: ("ApplicationNotes", None), # XPM Info 221 | 0x4746: ("Rating", None), 222 | 0x828D: ("CFARepeatPatternDim", None), 223 | 0x828E: ("CFAPattern", None), 224 | 0x828F: ("BatteryLevel", None), 225 | 0x8298: ("Copyright", None), 226 | 0x829A: ("ExposureTime", None), 227 | 0x829D: ("FNumber", None), 228 | 0x83BB: ("IPTC/NAA", None), 229 | 0x8769: ("ExifOffset", None), # Exif Tags 230 | 0x8773: ("InterColorProfile", None), 231 | 0x8822: ( 232 | "ExposureProgram", 233 | { 234 | 0: "Unidentified", 235 | 1: "Manual", 236 | 2: "Program Normal", 237 | 3: "Aperture Priority", 238 | 4: "Shutter Priority", 239 | 5: "Program Creative", 240 | 6: "Program Action", 241 | 7: "Portrait Mode", 242 | 8: "Landscape Mode", 243 | }, 244 | ), 245 | 0x8824: ("SpectralSensitivity", None), 246 | 0x8825: ("GPSInfo", GPS_INFO), # GPS tags 247 | 0x8827: ("ISOSpeedRatings", None), 248 | 0x8828: ("OECF", None), 249 | 0x8829: ("Interlace", None), 250 | 0x882A: ("TimeZoneOffset", None), 251 | 0x882B: ("SelfTimerMode", None), 252 | 0x8830: ( 253 | "SensitivityType", 254 | { 255 | 0: "Unknown", 256 | 1: "Standard Output Sensitivity", 257 | 2: "Recommended Exposure Index", 258 | 3: "ISO Speed", 259 | 4: "Standard Output Sensitivity and Recommended Exposure Index", 260 | 5: "Standard Output Sensitivity and ISO Speed", 261 | 6: "Recommended Exposure Index and ISO Speed", 262 | 7: "Standard Output Sensitivity, Recommended Exposure Index and ISO Speed", 263 | }, 264 | ), 265 | 0x8832: ("RecommendedExposureIndex", None), 266 | 0x8833: ("ISOSpeed", None), 267 | 0x9000: ("ExifVersion", make_string), 268 | 0x9003: ("DateTimeOriginal", None), 269 | 0x9004: ("DateTimeDigitized", None), 270 | 0x9010: ("OffsetTime", None), 271 | 0x9011: ("OffsetTimeOriginal", None), 272 | 0x9012: ("OffsetTimeDigitized", None), 273 | 0x9101: ( 274 | "ComponentsConfiguration", 275 | {0: "", 1: "Y", 2: "Cb", 3: "Cr", 4: "Red", 5: "Green", 6: "Blue"}, 276 | ), 277 | 0x9102: ("CompressedBitsPerPixel", None), 278 | 0x9201: ("ShutterSpeedValue", None), 279 | 0x9202: ("ApertureValue", None), 280 | 0x9203: ("BrightnessValue", None), 281 | 0x9204: ("ExposureBiasValue", None), 282 | 0x9205: ("MaxApertureValue", None), 283 | 0x9206: ("SubjectDistance", None), 284 | 0x9207: ( 285 | "MeteringMode", 286 | { 287 | 0: "Unidentified", 288 | 1: "Average", 289 | 2: "CenterWeightedAverage", 290 | 3: "Spot", 291 | 4: "MultiSpot", 292 | 5: "Pattern", 293 | 6: "Partial", 294 | 255: "other", 295 | }, 296 | ), 297 | 0x9208: ( 298 | "LightSource", 299 | { 300 | 0: "Unknown", 301 | 1: "Daylight", 302 | 2: "Fluorescent", 303 | 3: "Tungsten (incandescent light)", 304 | 4: "Flash", 305 | 9: "Fine weather", 306 | 10: "Cloudy weather", 307 | 11: "Shade", 308 | 12: "Daylight fluorescent (D 5700 - 7100K)", 309 | 13: "Day white fluorescent (N 4600 - 5400K)", 310 | 14: "Cool white fluorescent (W 3900 - 4500K)", 311 | 15: "White fluorescent (WW 3200 - 3700K)", 312 | 17: "Standard light A", 313 | 18: "Standard light B", 314 | 19: "Standard light C", 315 | 20: "D55", 316 | 21: "D65", 317 | 22: "D75", 318 | 23: "D50", 319 | 24: "ISO studio tungsten", 320 | 255: "other light source", 321 | }, 322 | ), 323 | 0x9209: ( 324 | "Flash", 325 | { 326 | 0: "Flash did not fire", 327 | 1: "Flash fired", 328 | 5: "Strobe return light not detected", 329 | 7: "Strobe return light detected", 330 | 9: "Flash fired, compulsory flash mode", 331 | 13: "Flash fired, compulsory flash mode, return light not detected", 332 | 15: "Flash fired, compulsory flash mode, return light detected", 333 | 16: "Flash did not fire, compulsory flash mode", 334 | 24: "Flash did not fire, auto mode", 335 | 25: "Flash fired, auto mode", 336 | 29: "Flash fired, auto mode, return light not detected", 337 | 31: "Flash fired, auto mode, return light detected", 338 | 32: "No flash function", 339 | 65: "Flash fired, red-eye reduction mode", 340 | 69: "Flash fired, red-eye reduction mode, return light not detected", 341 | 71: "Flash fired, red-eye reduction mode, return light detected", 342 | 73: "Flash fired, compulsory flash mode, red-eye reduction mode", 343 | 77: "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", 344 | 79: "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", 345 | 89: "Flash fired, auto mode, red-eye reduction mode", 346 | 93: "Flash fired, auto mode, return light not detected, red-eye reduction mode", 347 | 95: "Flash fired, auto mode, return light detected, red-eye reduction mode", 348 | }, 349 | ), 350 | 0x920A: ("FocalLength", None), 351 | 0x920B: ("FlashEnergy", None), 352 | 0x920C: ("SpatialFrequencyResponse", None), 353 | 0x920D: ("Noise", None), 354 | 0x9211: ("ImageNumber", None), 355 | 0x9212: ("SecurityClassification", None), 356 | 0x9213: ("ImageHistory", None), 357 | 0x9214: ("SubjectArea", None), 358 | 0x9215: ("ExposureIndex", None), 359 | 0x9216: ("TIFF/EPStandardID", None), 360 | 0x927C: ("MakerNote", None), 361 | 0x9286: ("UserComment", make_string_uc), 362 | 0x9290: ("SubSecTime", None), 363 | 0x9291: ("SubSecTimeOriginal", None), 364 | 0x9292: ("SubSecTimeDigitized", None), 365 | # used by Windows Explorer 366 | 0x9C9B: ("XPTitle", None), 367 | 0x9C9C: ("XPComment", None), 368 | 0x9C9D: ("XPAuthor", make_string), # (ignored by Windows Explorer if Artist exists) 369 | 0x9C9E: ("XPKeywords", None), 370 | 0x9C9F: ("XPSubject", None), 371 | 0xA000: ("FlashPixVersion", make_string), 372 | 0xA001: ("ColorSpace", {1: "sRGB", 2: "Adobe RGB", 65535: "Uncalibrated"}), 373 | 0xA002: ("ExifImageWidth", None), 374 | 0xA003: ("ExifImageLength", None), 375 | 0xA004: ("RelatedSoundFile", None), 376 | 0xA005: ("InteroperabilityOffset", INTEROP_INFO), 377 | 0xA20B: ("FlashEnergy", None), # 0x920B in TIFF/EP 378 | 0xA20C: ("SpatialFrequencyResponse", None), # 0x920C 379 | 0xA20E: ("FocalPlaneXResolution", None), # 0x920E 380 | 0xA20F: ("FocalPlaneYResolution", None), # 0x920F 381 | 0xA210: ("FocalPlaneResolutionUnit", None), # 0x9210 382 | 0xA214: ("SubjectLocation", None), # 0x9214 383 | 0xA215: ("ExposureIndex", None), # 0x9215 384 | 0xA217: ( 385 | "SensingMethod", 386 | { # 0x9217 387 | 1: "Not defined", 388 | 2: "One-chip color area", 389 | 3: "Two-chip color area", 390 | 4: "Three-chip color area", 391 | 5: "Color sequential area", 392 | 7: "Trilinear", 393 | 8: "Color sequential linear", 394 | }, 395 | ), 396 | 0xA300: ( 397 | "FileSource", 398 | {1: "Film Scanner", 2: "Reflection Print Scanner", 3: "Digital Camera"}, 399 | ), 400 | 0xA301: ("SceneType", {1: "Directly Photographed"}), 401 | 0xA302: ("CVAPattern", None), 402 | 0xA401: ("CustomRendered", {0: "Normal", 1: "Custom"}), 403 | 0xA402: ( 404 | "ExposureMode", 405 | {0: "Auto Exposure", 1: "Manual Exposure", 2: "Auto Bracket"}, 406 | ), 407 | 0xA403: ("WhiteBalance", {0: "Auto", 1: "Manual"}), 408 | 0xA404: ("DigitalZoomRatio", None), 409 | 0xA405: ("FocalLengthIn35mmFilm", None), 410 | 0xA406: ( 411 | "SceneCaptureType", 412 | {0: "Standard", 1: "Landscape", 2: "Portrait", 3: "Night"}, 413 | ), 414 | 0xA407: ( 415 | "GainControl", 416 | { 417 | 0: "None", 418 | 1: "Low gain up", 419 | 2: "High gain up", 420 | 3: "Low gain down", 421 | 4: "High gain down", 422 | }, 423 | ), 424 | 0xA408: ("Contrast", {0: "Normal", 1: "Soft", 2: "Hard"}), 425 | 0xA409: ("Saturation", {0: "Normal", 1: "Soft", 2: "Hard"}), 426 | 0xA40A: ("Sharpness", {0: "Normal", 1: "Soft", 2: "Hard"}), 427 | 0xA40B: ("DeviceSettingDescription", None), 428 | 0xA40C: ("SubjectDistanceRange", None), 429 | 0xA420: ("ImageUniqueID", None), 430 | 0xA430: ("CameraOwnerName", None), 431 | 0xA431: ("BodySerialNumber", None), 432 | 0xA432: ("LensSpecification", None), 433 | 0xA433: ("LensMake", None), 434 | 0xA434: ("LensModel", None), 435 | 0xA435: ("LensSerialNumber", None), 436 | 0xA500: ("Gamma", None), 437 | 0xC4A5: ("PrintIM", None), 438 | 0xC61A: ("BlackLevel", None), 439 | 0xEA1C: ("Padding", None), 440 | 0xEA1D: ("OffsetSchema", None), 441 | 0xFDE8: ("OwnerName", None), 442 | 0xFDE9: ("SerialNumber", None), 443 | } 444 | -------------------------------------------------------------------------------- /exifread/tags/fields.py: -------------------------------------------------------------------------------- 1 | """Field type definitions.""" 2 | 3 | from enum import IntEnum 4 | from typing import Dict, List, Tuple 5 | 6 | 7 | class FieldType(IntEnum): 8 | """Field types mapped to their integer representation.""" 9 | 10 | PROPRIETARY = 0 11 | BYTE = 1 12 | ASCII = 2 13 | SHORT = 3 14 | LONG = 4 15 | RATIO = 5 16 | SIGNED_BYTE = 6 17 | UNDEFINED = 7 18 | SIGNED_SHORT = 8 19 | SIGNED_LONG = 9 20 | SIGNED_RATIO = 10 21 | FLOAT_32 = 11 22 | FLOAT_64 = 12 23 | IFD = 13 24 | 25 | 26 | SIGNED_FIELD_TYPES: List[FieldType] = [ 27 | FieldType.SIGNED_BYTE, 28 | FieldType.SIGNED_SHORT, 29 | FieldType.SIGNED_LONG, 30 | FieldType.SIGNED_RATIO, 31 | ] 32 | 33 | RATIO_FIELD_TYPES: List[FieldType] = [FieldType.RATIO, FieldType.SIGNED_RATIO] 34 | 35 | FLOAT_FIELD_TYPES: List[FieldType] = [FieldType.FLOAT_32, FieldType.FLOAT_64] 36 | 37 | # Field type descriptions as (length, full name) tuples 38 | FIELD_DEFINITIONS: Dict[FieldType, Tuple[int, str]] = { 39 | FieldType.PROPRIETARY: (0, "Proprietary"), 40 | FieldType.BYTE: (1, "Byte"), 41 | FieldType.ASCII: (1, "ASCII"), 42 | FieldType.SHORT: (2, "Short"), 43 | FieldType.LONG: (4, "Long"), 44 | FieldType.RATIO: (8, "Ratio"), 45 | FieldType.SIGNED_BYTE: (1, "Signed Byte"), 46 | FieldType.UNDEFINED: (1, "Undefined"), 47 | FieldType.SIGNED_SHORT: (2, "Signed Short"), 48 | FieldType.SIGNED_LONG: (4, "Signed Long"), 49 | FieldType.SIGNED_RATIO: (8, "Signed Ratio"), 50 | FieldType.FLOAT_32: (4, "Single-Precision Floating Point (32-bit)"), 51 | FieldType.FLOAT_64: (8, "Double-Precision Floating Point (64-bit)"), 52 | FieldType.IFD: (4, "IFD"), 53 | } 54 | -------------------------------------------------------------------------------- /exifread/tags/makernote/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Makernote tag definitions. 3 | """ 4 | -------------------------------------------------------------------------------- /exifread/tags/makernote/apple.py: -------------------------------------------------------------------------------- 1 | """ 2 | Makernote (proprietary) tag definitions for Apple iOS 3 | 4 | Based on version 1.01 of ExifTool -> Image/ExifTool/Apple.pm 5 | http://owl.phy.queensu.ca/~phil/exiftool/ 6 | """ 7 | 8 | from exifread.tags import SubIfdTagDict 9 | 10 | TAGS: SubIfdTagDict = { 11 | 0x0001: ("MakerNoteVersion", None), 12 | 0x0004: ( 13 | "AEStable", 14 | { 15 | 0: "No", 16 | 1: "Yes", 17 | }, 18 | ), 19 | 0x0005: ("AETarget", None), 20 | 0x0006: ("AEAverage", None), 21 | 0x0007: ( 22 | "AFStable", 23 | { 24 | 0: "No", 25 | 1: "Yes", 26 | }, 27 | ), 28 | 0x000A: ( 29 | "HDRImageType", 30 | { 31 | 3: "HDR Image", 32 | 4: "Original Image", 33 | }, 34 | ), 35 | 0x0014: ( 36 | "ImageCaptureType", 37 | { 38 | 1: "ProRAW", 39 | 2: "Portrait", 40 | 10: "Photo", 41 | 11: "Manual Focus", 42 | 12: "Scene", 43 | }, 44 | ), 45 | 0x0015: ("ImageUniqueID", None), 46 | 0x002E: ( 47 | "CameraType", 48 | { 49 | 0: "Back Wide Angle", 50 | 1: "Back Normal", 51 | 6: "Front", 52 | }, 53 | ), 54 | } 55 | -------------------------------------------------------------------------------- /exifread/tags/makernote/casio.py: -------------------------------------------------------------------------------- 1 | """ 2 | Makernote (proprietary) tag definitions for Casio. 3 | """ 4 | 5 | from exifread.tags import SubIfdTagDict 6 | 7 | TAGS: SubIfdTagDict = { 8 | 0x0001: ( 9 | "RecordingMode", 10 | { 11 | 1: "Single Shutter", 12 | 2: "Panorama", 13 | 3: "Night Scene", 14 | 4: "Portrait", 15 | 5: "Landscape", 16 | }, 17 | ), 18 | 0x0002: ( 19 | "Quality", 20 | { 21 | 1: "Economy", 22 | 2: "Normal", 23 | 3: "Fine", 24 | }, 25 | ), 26 | 0x0003: ( 27 | "FocusingMode", 28 | { 29 | 2: "Macro", 30 | 3: "Auto Focus", 31 | 4: "Manual Focus", 32 | 5: "Infinity", 33 | }, 34 | ), 35 | 0x0004: ( 36 | "FlashMode", 37 | { 38 | 1: "Auto", 39 | 2: "On", 40 | 3: "Off", 41 | 4: "Red Eye Reduction", 42 | }, 43 | ), 44 | 0x0005: ( 45 | "FlashIntensity", 46 | { 47 | 11: "Weak", 48 | 13: "Normal", 49 | 15: "Strong", 50 | }, 51 | ), 52 | 0x0006: ("Object Distance", None), 53 | 0x0007: ( 54 | "WhiteBalance", 55 | { 56 | 1: "Auto", 57 | 2: "Tungsten", 58 | 3: "Daylight", 59 | 4: "Fluorescent", 60 | 5: "Shade", 61 | 129: "Manual", 62 | }, 63 | ), 64 | 0x000A: ("DigitalZoom", None), 65 | 0x000B: ( 66 | "Sharpness", 67 | { 68 | 0: "Normal", 69 | 1: "Soft", 70 | 2: "Hard", 71 | }, 72 | ), 73 | 0x000C: ( 74 | "Contrast", 75 | { 76 | 0: "Normal", 77 | 1: "Low", 78 | 2: "High", 79 | }, 80 | ), 81 | 0x000D: ( 82 | "Saturation", 83 | { 84 | 0: "Normal", 85 | 1: "Low", 86 | 2: "High", 87 | }, 88 | ), 89 | 0x0014: ( 90 | "CCDSpeed", 91 | { 92 | 64: "Normal", 93 | 80: "Normal", 94 | 100: "High", 95 | 125: "+1.0", 96 | 244: "+3.0", 97 | 250: "+2.0", 98 | }, 99 | ), 100 | 0x0015: ("FirmwareDate", None), 101 | } 102 | -------------------------------------------------------------------------------- /exifread/tags/makernote/dji.py: -------------------------------------------------------------------------------- 1 | """ 2 | Makernote (proprietary) tag definitions for DJI cameras 3 | 4 | Based on https://github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/DJI.pm 5 | """ 6 | 7 | from exifread.tags import SubIfdTagDict 8 | 9 | TAGS: SubIfdTagDict = { 10 | 0x01: ("Make", None), 11 | 0x03: ("SpeedX", None), 12 | 0x04: ("SpeedY", None), 13 | 0x05: ("SpeedZ", None), 14 | 0x06: ("Pitch", None), 15 | 0x07: ("Yaw", None), 16 | 0x08: ("Roll", None), 17 | 0x09: ("CameraPitch", None), 18 | 0x0A: ("CameraYaw", None), 19 | 0x0B: ("CameraRoll", None), 20 | } 21 | -------------------------------------------------------------------------------- /exifread/tags/makernote/fujifilm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Makernote (proprietary) tag definitions for FujiFilm. 3 | 4 | http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/FujiFilm.html 5 | """ 6 | 7 | from exifread.tags import SubIfdTagDict 8 | from exifread.tags.str_utils import make_string 9 | 10 | TAGS: SubIfdTagDict = { 11 | 0x0000: ("NoteVersion", make_string), 12 | 0x0010: ("InternalSerialNumber", None), 13 | 0x1000: ("Quality", None), 14 | 0x1001: ( 15 | "Sharpness", 16 | { 17 | 0x1: "Soft", 18 | 0x2: "Soft", 19 | 0x3: "Normal", 20 | 0x4: "Hard", 21 | 0x5: "Hard2", 22 | 0x82: "Medium Soft", 23 | 0x84: "Medium Hard", 24 | 0x8000: "Film Simulation", 25 | }, 26 | ), 27 | 0x1002: ( 28 | "WhiteBalance", 29 | { 30 | 0x0: "Auto", 31 | 0x100: "Daylight", 32 | 0x200: "Cloudy", 33 | 0x300: "Daylight Fluorescent", 34 | 0x301: "Day White Fluorescent", 35 | 0x302: "White Fluorescent", 36 | 0x303: "Warm White Fluorescent", 37 | 0x304: "Living Room Warm White Fluorescent", 38 | 0x400: "Incandescent", 39 | 0x500: "Flash", 40 | 0x600: "Underwater", 41 | 0xF00: "Custom", 42 | 0xF01: "Custom2", 43 | 0xF02: "Custom3", 44 | 0xF03: "Custom4", 45 | 0xF04: "Custom5", 46 | 0xFF0: "Kelvin", 47 | }, 48 | ), 49 | 0x1003: ( 50 | "Saturation", 51 | { 52 | 0x0: "Normal", 53 | 0x80: "Medium High", 54 | 0x100: "High", 55 | 0x180: "Medium Low", 56 | 0x200: "Low", 57 | 0x300: "None (B&W)", 58 | 0x301: "B&W Red Filter", 59 | 0x302: "B&W Yellow Filter", 60 | 0x303: "B&W Green Filter", 61 | 0x310: "B&W Sepia", 62 | 0x400: "Low 2", 63 | 0x8000: "Film Simulation", 64 | }, 65 | ), 66 | 0x1004: ( 67 | "Contrast", 68 | { 69 | 0x0: "Normal", 70 | 0x80: "Medium High", 71 | 0x100: "High", 72 | 0x180: "Medium Low", 73 | 0x200: "Low", 74 | 0x8000: "Film Simulation", 75 | }, 76 | ), 77 | 0x1005: ("ColorTemperature", None), 78 | 0x1006: ( 79 | "Contrast", 80 | { 81 | 0x0: "Normal", 82 | 0x100: "High", 83 | 0x300: "Low", 84 | }, 85 | ), 86 | 0x100A: ("WhiteBalanceFineTune", None), 87 | 0x1010: ( 88 | "FlashMode", 89 | { 90 | 0: "Auto", 91 | 1: "On", 92 | 2: "Off", 93 | 3: "Red Eye Reduction", 94 | }, 95 | ), 96 | 0x1011: ("FlashStrength", None), 97 | 0x1020: ("Macro", {0: "Off", 1: "On"}), 98 | 0x1021: ("FocusMode", {0: "Auto", 1: "Manual"}), 99 | 0x1022: ("AFPointSet", {0: "Yes", 1: "No"}), 100 | 0x1023: ("FocusPixel", None), 101 | 0x1030: ("SlowSync", {0: "Off", 1: "On"}), 102 | 0x1031: ( 103 | "PictureMode", 104 | { 105 | 0: "Auto", 106 | 1: "Portrait", 107 | 2: "Landscape", 108 | 4: "Sports", 109 | 5: "Night", 110 | 6: "Program AE", 111 | 256: "Aperture Priority AE", 112 | 512: "Shutter Priority AE", 113 | 768: "Manual Exposure", 114 | }, 115 | ), 116 | 0x1032: ("ExposureCount", None), 117 | 0x1100: ("MotorOrBracket", {0: "Off", 1: "On"}), 118 | 0x1210: ( 119 | "ColorMode", 120 | { 121 | 0x0: "Standard", 122 | 0x10: "Chrome", 123 | 0x30: "B & W", 124 | }, 125 | ), 126 | 0x1300: ("BlurWarning", {0: "Off", 1: "On"}), 127 | 0x1301: ("FocusWarning", {0: "Off", 1: "On"}), 128 | 0x1302: ("ExposureWarning", {0: "Off", 1: "On"}), 129 | } 130 | -------------------------------------------------------------------------------- /exifread/tags/makernote/nikon.py: -------------------------------------------------------------------------------- 1 | """ 2 | Makernote (proprietary) tag definitions for Nikon. 3 | """ 4 | 5 | from exifread.tags import SubIfdTagDict 6 | from exifread.tags.str_utils import make_string 7 | from exifread.utils import Ratio 8 | 9 | 10 | def ev_bias(seq) -> str: 11 | """ 12 | First digit seems to be in steps of 1/6 EV. 13 | Does the third value mean the step size? It is usually 6, 14 | but it is 12 for the ExposureDifference. 15 | Check for an error condition that could cause a crash. 16 | This only happens if something has gone really wrong in 17 | reading the Nikon MakerNote. 18 | http://tomtia.plala.jp/DigitalCamera/MakerNote/index.asp 19 | """ 20 | 21 | if len(seq) < 4: 22 | return "" 23 | if seq == [252, 1, 6, 0]: 24 | return "-2/3 EV" 25 | if seq == [253, 1, 6, 0]: 26 | return "-1/2 EV" 27 | if seq == [254, 1, 6, 0]: 28 | return "-1/3 EV" 29 | if seq == [0, 1, 6, 0]: 30 | return "0 EV" 31 | if seq == [2, 1, 6, 0]: 32 | return "+1/3 EV" 33 | if seq == [3, 1, 6, 0]: 34 | return "+1/2 EV" 35 | if seq == [4, 1, 6, 0]: 36 | return "+2/3 EV" 37 | # Handle combinations not in the table. 38 | i = seq[0] 39 | # Causes headaches for the +/- logic, so special case it. 40 | if i == 0: 41 | return "0 EV" 42 | if i > 127: 43 | i = 256 - i 44 | ret_str = "-" 45 | else: 46 | ret_str = "+" 47 | step = seq[2] # Assume third value means the step size 48 | whole = i / step 49 | i = i % step 50 | if whole != 0: 51 | ret_str = "%s%s " % (ret_str, str(whole)) 52 | if i == 0: 53 | ret_str += "EV" 54 | else: 55 | ratio = Ratio(i, step) 56 | ret_str = ret_str + str(ratio) + " EV" 57 | return ret_str 58 | 59 | 60 | # Nikon E99x MakerNote Tags 61 | TAGS_NEW: SubIfdTagDict = { 62 | 0x0001: ("MakernoteVersion", make_string), # Sometimes binary 63 | 0x0002: ("ISOSetting", None), 64 | 0x0003: ("ColorMode", None), 65 | 0x0004: ("Quality", None), 66 | 0x0005: ("Whitebalance", None), 67 | 0x0006: ("ImageSharpening", None), 68 | 0x0007: ("FocusMode", None), 69 | 0x0008: ("FlashSetting", None), 70 | 0x0009: ("AutoFlashMode", None), 71 | 0x000A: ("LensMount", None), # According to LibRAW 72 | 0x000B: ("WhiteBalanceBias", None), 73 | 0x000C: ("WhiteBalanceRBCoeff", None), 74 | 0x000D: ("ProgramShift", ev_bias), 75 | # Nearly the same as the other EV vals, but step size is 1/12 EV (?) 76 | 0x000E: ("ExposureDifference", ev_bias), 77 | 0x000F: ("ISOSelection", None), 78 | 0x0010: ("DataDump", None), 79 | 0x0011: ("NikonPreview", None), 80 | 0x0012: ("FlashCompensation", ev_bias), 81 | 0x0013: ("ISOSpeedRequested", None), 82 | 0x0014: ("ColorBalance", None), # According to LibRAW 83 | 0x0016: ("PhotoCornerCoordinates", None), 84 | 0x0017: ("ExternalFlashExposureComp", ev_bias), 85 | 0x0018: ("FlashBracketCompensationApplied", ev_bias), 86 | 0x0019: ("AEBracketCompensationApplied", None), 87 | 0x001A: ("ImageProcessing", None), 88 | 0x001B: ( 89 | "CropHiSpeed", 90 | None, 91 | # TODO: investigate, returns incoherent results 92 | # 93 | # { 94 | # 0: "Off", 95 | # 1: "1.3x Crop", 96 | # 2: "DX Crop", 97 | # 3: "5:4 Crop", 98 | # 4: "3:2 Crop", 99 | # 6: "16:9 Crop", 100 | # 8: "2.7x Crop", 101 | # 9: "DX Movie Crop", 102 | # 10: "1.3x Movie Crop", 103 | # 11: "FX Uncropped", 104 | # 12: "DX Uncropped", 105 | # 13: "2.8x Movie Crop", 106 | # 14: "1.4x Movie Crop", 107 | # 15: "1.5x Movie Crop", 108 | # 17: "1:1 Crop", 109 | # } 110 | ), 111 | 0x001C: ("ExposureTuning", None), 112 | 0x001D: ("SerialNumber", None), # Conflict with 0x00A0 ? 113 | 0x001E: ( 114 | "ColorSpace", 115 | { 116 | 1: "sRGB", 117 | 2: "Adobe RGB", 118 | 4: "BT.2100", 119 | }, 120 | ), 121 | 0x001F: ("VRInfo", None), 122 | 0x0020: ("ImageAuthentication", None), 123 | 0x0021: ("FaceDetect", None), 124 | 0x0022: ( 125 | "ActiveDLighting", 126 | { 127 | 0: "Off", 128 | 1: "Low", 129 | 3: "Normal", 130 | 5: "High", 131 | 7: "Extra High", 132 | 8: "Extra High 1", 133 | 9: "Extra High 2", 134 | 10: "Extra High 3", 135 | 11: "Extra High 4", 136 | 65535: "Auto", 137 | }, 138 | ), 139 | 0x0023: ("PictureControl", None), 140 | 0x0024: ("WorldTime", None), 141 | 0x0025: ("ISOInfo", None), 142 | 0x002A: ( 143 | "VignetteControl", 144 | { 145 | 0: "Off", 146 | 1: "Low", 147 | 3: "Normal", 148 | 5: "High", 149 | }, 150 | ), 151 | 0x002B: ("DistortInfo", None), 152 | 0x002C: ("UnknownInfo", None), 153 | 0x0032: ("UnknownInfo2", None), 154 | 0x0034: ( 155 | "ShutterMode", 156 | { 157 | 0: "Mechanical", 158 | 16: "Electronic", 159 | 48: "Electronic Front Curtain", 160 | 64: "Electronic (Movie)", 161 | 80: "Auto (Mechanical)", 162 | 81: "Auto (Electronic Front Curtain)", 163 | 96: "Electronic (High Speed)", 164 | }, 165 | ), 166 | 0x0035: ("HDRInfo", None), 167 | 0x0037: ("MechanicalShutterCount", None), 168 | 0x0039: ("LocationInfo", None), 169 | 0x003B: ("MultiExposureWhiteBalance", None), 170 | 0x003D: ("BlackLevel", None), 171 | 0x003E: ("ImageSizeRAW", None), 172 | 0x0045: ("CropArea", None), 173 | 0x004E: ("NikonSettings", None), 174 | 0x004F: ("ColorTemperatureAuto", None), 175 | 0x0080: ("ImageAdjustment", None), 176 | 0x0081: ("ToneCompensation", None), 177 | 0x0082: ("AuxiliaryLens", None), 178 | 0x0083: ("LensType", None), 179 | 0x0084: ("LensMinMaxFocalMaxAperture", None), 180 | 0x0085: ("ManualFocusDistance", None), 181 | 0x0086: ("DigitalZoomFactor", None), 182 | 0x0087: ( 183 | "FlashMode", 184 | { 185 | 0: "Did Not Fire", 186 | 1: "Fired, Manual", 187 | 3: "Not Ready", 188 | 7: "Fired, External", 189 | 8: "Fired, Commander Mode ", 190 | 9: "Fired, TTL Mode", 191 | 18: "LED Light", 192 | }, 193 | ), 194 | 0x0088: ( 195 | "AFFocusPosition", 196 | { 197 | 0x0000: "Center", 198 | 0x0100: "Top", 199 | 0x0200: "Bottom", 200 | 0x0300: "Left", 201 | 0x0400: "Right", 202 | }, 203 | ), 204 | 0x0089: ( 205 | "BracketingMode", 206 | { 207 | 0x00: "Single frame, no bracketing", 208 | 0x01: "Continuous, no bracketing", 209 | 0x02: "Timer, no bracketing", 210 | 0x10: "Single frame, exposure bracketing", 211 | 0x11: "Continuous, exposure bracketing", 212 | 0x12: "Timer, exposure bracketing", 213 | 0x40: "Single frame, white balance bracketing", 214 | 0x41: "Continuous, white balance bracketing", 215 | 0x42: "Timer, white balance bracketing", 216 | }, 217 | ), 218 | 0x008A: ("AutoBracketRelease", None), 219 | 0x008B: ("LensFStops", None), 220 | 0x008C: ("NEFCurve1", None), # ExifTool calls this 'ContrastCurve' 221 | 0x008D: ("ColorMode", None), 222 | 0x008F: ("SceneMode", None), 223 | 0x0090: ("LightingType", None), 224 | 0x0091: ("ShotInfo", None), # First 4 bytes are a version number in ASCII 225 | 0x0092: ("HueAdjustment", None), 226 | 0x0093: ( 227 | "NEFCompression", 228 | { 229 | 1: "Lossy (type 1)", 230 | 2: "Uncompressed", 231 | 3: "Lossless", 232 | 4: "Lossy (type 2)", 233 | 5: "Striped packed 12 bits", 234 | 6: "Uncompressed (reduced to 12 bit)", 235 | 7: "Unpacked 12 bits", 236 | 8: "Small", 237 | 9: "Packed 12 bits", 238 | 10: "Packed 14 bits", 239 | 13: "High Efficiency", 240 | 14: "High Efficiency*", 241 | }, 242 | ), 243 | 0x0094: ( 244 | "Saturation", 245 | { 246 | -3: "B&W", 247 | -2: "-2", 248 | -1: "-1", 249 | 0: "0", 250 | 1: "1", 251 | 2: "2", 252 | }, 253 | ), 254 | 0x0095: ("NoiseReduction", None), 255 | 0x0096: ("NEFCurve2", None), # ExifTool calls this 'LinearizationTable' 256 | 0x0097: ("ColorBalance", None), # First 4 bytes are a version number in ASCII 257 | 0x0098: ("LensData", None), # First 4 bytes are a version number in ASCII 258 | 0x0099: ("RawImageCenter", None), 259 | 0x009A: ("SensorPixelSize", None), 260 | 0x009C: ("Scene Assist", None), 261 | 0x009D: ( 262 | "DateStampMode", 263 | {0: "Off", 1: "Date & Time", 2: "Date", 3: "Date Counter"}, 264 | ), 265 | 0x009E: ( 266 | "RetouchHistory", 267 | None, 268 | # TODO: investigate, returns incoherent results 269 | # 270 | # { 271 | # 0: "None", 272 | # 3: "B & W", 273 | # 4: "Sepia", 274 | # 5: "Trim", 275 | # 6: "Small Picture", 276 | # 7: "D-Lighting", 277 | # 8: "Red Eye", 278 | # 9: "Cyanotype", 279 | # 10: "Sky Light", 280 | # 11: "Warm Tone", 281 | # 12: "Color Custom", 282 | # 13: "Image Overlay", 283 | # 14: "Red Intensifier", 284 | # 15: "Green Intensifier", 285 | # 16: "Blue Intensifier", 286 | # 17: "Cross Screen", 287 | # 18: "Quick Retouch", 288 | # 19: "NEF Processing", 289 | # 23: "Distortion Control", 290 | # 25: "Fisheye", 291 | # 26: "Straighten", 292 | # 29: "Perspective Control", 293 | # 30: "Color Outline", 294 | # 31: "Soft Filter", 295 | # 32: "Resize", 296 | # 33: "Miniature Effect", 297 | # 34: "Skin Softening", 298 | # 35: "Selected Frame", 299 | # 37: "Color Sketch", 300 | # 38: "Selective Color", 301 | # 39: "Glamour", 302 | # 40: "Drawing", 303 | # 44: "Pop", 304 | # 45: "Toy Camera Effect 1", 305 | # 46: "Toy Camera Effect 2", 306 | # 47: "Cross Process (red)", 307 | # 48: "Cross Process (blue)", 308 | # 49: "Cross Process (green)", 309 | # 50: "Cross Process (yellow)", 310 | # 51: "Super Vivid", 311 | # 52: "High-contrast Monochrome", 312 | # 53: "High Key", 313 | # 54: "Low Key", 314 | # }, 315 | ), 316 | 0x00A0: ("SerialNumber", None), 317 | 0x00A2: ("ImageDataSize", None), 318 | # 00A3: unknown - a single byte 0 319 | # 00A4: In NEF, looks like a 4 byte ASCII version number ('0200') 320 | 0x00A5: ("ImageCount", None), 321 | 0x00A6: ("DeletedImageCount", None), 322 | 0x00A7: ("TotalShutterReleases", None), 323 | # First 4 bytes are a version number in ASCII, with version specific 324 | # info to follow. It's hard to treat it as a string due to embedded nulls. 325 | 0x00A8: ("FlashInfo", None), 326 | 0x00A9: ("ImageOptimization", None), 327 | 0x00AA: ("Saturation", None), 328 | 0x00AB: ("DigitalVariProgram", None), 329 | 0x00AC: ("ImageStabilization", None), 330 | 0x00AD: ("AFResponse", None), 331 | 0x00B0: ("MultiExposure", None), 332 | 0x00B1: ( 333 | "HighISONoiseReduction", 334 | { 335 | 0: "Off", 336 | 1: "Minimal", 337 | 2: "Low", 338 | 3: "Medium Low", 339 | 4: "Normal", 340 | 5: "Medium High", 341 | 6: "High", 342 | }, 343 | ), 344 | 0x00B3: ("ToningEffect", None), 345 | 0x00B6: ("PowerUpTime", None), 346 | 0x00B7: ("AFInfo2", None), 347 | 0x00B8: ("FileInfo", None), 348 | 0x00B9: ("AFTune", None), 349 | 0x00BB: ("RetouchInfo", None), 350 | 0x00BD: ("PictureControlData", None), 351 | 0x00BF: ("SilentPhotography", {0: "Off", 1: "On"}), 352 | 0x00C3: ("BarometerInfo", None), 353 | 0x0100: ("DigitalICE", None), 354 | 0x0201: ("PreviewImageStart", None), 355 | 0x0202: ("PreviewImageLength", None), 356 | 0x0213: ( 357 | "PreviewYCbCrPositioning", 358 | { 359 | 1: "Centered", 360 | 2: "Co-sited", 361 | }, 362 | ), 363 | 0x0E00: ("PrintIM", None), 364 | 0x0E01: ("InCameraEditNote", None), 365 | 0x0E09: ("NikonCaptureVersion", None), 366 | 0x0E0E: ("NikonCaptureOffsets", None), 367 | 0x0E10: ("NikonScan", None), 368 | 0x0E13: ("NikonCaptureEditVersions", None), 369 | 0x0E1D: ("NikonICCProfile", None), 370 | 0x0E1E: ("NikonCaptureOutput", None), 371 | 0x0E22: ("NEFBitDepth", None), 372 | } 373 | 374 | TAGS_OLD: SubIfdTagDict = { 375 | 0x0003: ( 376 | "Quality", 377 | { 378 | 1: "VGA Basic", 379 | 2: "VGA Normal", 380 | 3: "VGA Fine", 381 | 4: "SXGA Basic", 382 | 5: "SXGA Normal", 383 | 6: "SXGA Fine", 384 | }, 385 | ), 386 | 0x0004: ( 387 | "ColorMode", 388 | { 389 | 1: "Color", 390 | 2: "Monochrome", 391 | }, 392 | ), 393 | 0x0005: ( 394 | "ImageAdjustment", 395 | { 396 | 0: "Normal", 397 | 1: "Bright+", 398 | 2: "Bright-", 399 | 3: "Contrast+", 400 | 4: "Contrast-", 401 | }, 402 | ), 403 | 0x0006: ( 404 | "CCDSpeed", 405 | { 406 | 0: "ISO 80", 407 | 2: "ISO 160", 408 | 4: "ISO 320", 409 | 5: "ISO 100", 410 | }, 411 | ), 412 | 0x0007: ( 413 | "WhiteBalance", 414 | { 415 | 0: "Auto", 416 | 1: "Preset", 417 | 2: "Daylight", 418 | 3: "Incandescent", 419 | 4: "Fluorescent", 420 | 5: "Cloudy", 421 | 6: "Speed Light", 422 | }, 423 | ), 424 | } 425 | -------------------------------------------------------------------------------- /exifread/tags/makernote/olympus.py: -------------------------------------------------------------------------------- 1 | """ 2 | Makernote (proprietary) tag definitions for Olympus. 3 | """ 4 | 5 | from typing import Dict 6 | 7 | from exifread.tags import SubIfdTagDictValue 8 | from exifread.tags.str_utils import make_string 9 | 10 | 11 | def special_mode(val: bytes) -> str: 12 | """Decode Olympus SpecialMode tag in MakerNote""" 13 | mode1 = { 14 | 0: "Normal", 15 | 1: "Unknown", 16 | 2: "Fast", 17 | 3: "Panorama", 18 | } 19 | mode2 = { 20 | 0: "Non-panoramic", 21 | 1: "Left to right", 22 | 2: "Right to left", 23 | 3: "Bottom to top", 24 | 4: "Top to bottom", 25 | } 26 | 27 | if not val: 28 | return "" 29 | 30 | mode1_val = mode1.get(val[0], "Unknown") 31 | mode2_val = mode2.get(val[2], "Unknown") 32 | return "%s - Sequence %d - %s" % (mode1_val, val[1], mode2_val) 33 | 34 | 35 | TAGS: Dict[int, SubIfdTagDictValue] = { 36 | # Ah HAH! those sneeeeeaky bastids! this is how they get past the fact 37 | # that a JPEG thumbnail is not allowed in an uncompressed TIFF file 38 | 0x0100: ("JPEGThumbnail", None), 39 | 0x0200: ("SpecialMode", special_mode), 40 | 0x0201: ( 41 | "JPEGQual", 42 | { 43 | 1: "SQ", 44 | 2: "HQ", 45 | 3: "SHQ", 46 | }, 47 | ), 48 | 0x0202: ( 49 | "Macro", 50 | { 51 | 0: "Normal", 52 | 1: "Macro", 53 | 2: "SuperMacro", 54 | }, 55 | ), 56 | 0x0203: ("BWMode", {0: "Off", 1: "On"}), 57 | 0x0204: ("DigitalZoom", None), 58 | 0x0205: ("FocalPlaneDiagonal", None), 59 | 0x0206: ("LensDistortionParams", None), 60 | 0x0207: ("SoftwareRelease", None), 61 | 0x0208: ("PictureInfo", None), 62 | 0x0209: ("CameraID", make_string), # print as string 63 | 0x0F00: ("DataDump", None), 64 | 0x0300: ("PreCaptureFrames", None), 65 | 0x0404: ("SerialNumber", None), 66 | 0x1000: ("ShutterSpeedValue", None), 67 | 0x1001: ("ISOValue", None), 68 | 0x1002: ("ApertureValue", None), 69 | 0x1003: ("BrightnessValue", None), 70 | 0x1004: ("FlashMode", {2: "On", 3: "Off"}), 71 | 0x1005: ( 72 | "FlashDevice", 73 | { 74 | 0: "None", 75 | 1: "Internal", 76 | 4: "External", 77 | 5: "Internal + External", 78 | }, 79 | ), 80 | 0x1006: ("ExposureCompensation", None), 81 | 0x1007: ("SensorTemperature", None), 82 | 0x1008: ("LensTemperature", None), 83 | 0x100B: ("FocusMode", {0: "Auto", 1: "Manual"}), 84 | 0x1017: ("RedBalance", None), 85 | 0x1018: ("BlueBalance", None), 86 | 0x101A: ("SerialNumber", None), 87 | 0x1023: ("FlashExposureComp", None), 88 | 0x1026: ("ExternalFlashBounce", {0: "No", 1: "Yes"}), 89 | 0x1027: ("ExternalFlashZoom", None), 90 | 0x1028: ("ExternalFlashMode", None), 91 | 0x1029: ("Contrast int16u", {0: "High", 1: "Normal", 2: "Low"}), 92 | 0x102A: ("SharpnessFactor", None), 93 | 0x102B: ("ColorControl", None), 94 | 0x102C: ("ValidBits", None), 95 | 0x102D: ("CoringFilter", None), 96 | 0x102E: ("OlympusImageWidth", None), 97 | 0x102F: ("OlympusImageHeight", None), 98 | 0x1034: ("CompressionRatio", None), 99 | 0x1035: ("PreviewImageValid", {0: "No", 1: "Yes"}), 100 | 0x1036: ("PreviewImageStart", None), 101 | 0x1037: ("PreviewImageLength", None), 102 | 0x1039: ("CCDScanMode", {0: "Interlaced", 1: "Progressive"}), 103 | 0x103A: ("NoiseReduction", {0: "Off", 1: "On"}), 104 | 0x103B: ("InfinityLensStep", None), 105 | 0x103C: ("NearLensStep", None), 106 | # TODO - these need extra definitions 107 | # http://search.cpan.org/src/EXIFTOOL/Image-ExifTool-6.90/html/TagNames/Olympus.html 108 | 0x2010: ("Equipment", None), 109 | 0x2020: ("CameraSettings", None), 110 | 0x2030: ("RawDevelopment", None), 111 | 0x2040: ("ImageProcessing", None), 112 | 0x2050: ("FocusInfo", None), 113 | 0x3000: ("RawInfo ", None), 114 | } 115 | 116 | # 0x2020 CameraSettings 117 | TAG_0x2020: Dict[int, SubIfdTagDictValue] = { 118 | 0x0100: ("PreviewImageValid", {0: "No", 1: "Yes"}), 119 | 0x0101: ("PreviewImageStart", None), 120 | 0x0102: ("PreviewImageLength", None), 121 | 0x0200: ( 122 | "ExposureMode", 123 | { 124 | 1: "Manual", 125 | 2: "Program", 126 | 3: "Aperture-priority AE", 127 | 4: "Shutter speed priority AE", 128 | 5: "Program-shift", 129 | }, 130 | ), 131 | 0x0201: ("AELock", {0: "Off", 1: "On"}), 132 | 0x0202: ( 133 | "MeteringMode", 134 | { 135 | 2: "Center Weighted", 136 | 3: "Spot", 137 | 5: "ESP", 138 | 261: "Pattern+AF", 139 | 515: "Spot+Highlight control", 140 | 1027: "Spot+Shadow control", 141 | }, 142 | ), 143 | 0x0300: ("MacroMode", {0: "Off", 1: "On"}), 144 | 0x0301: ( 145 | "FocusMode", 146 | { 147 | 0: "Single AF", 148 | 1: "Sequential shooting AF", 149 | 2: "Continuous AF", 150 | 3: "Multi AF", 151 | 10: "MF", 152 | }, 153 | ), 154 | 0x0302: ( 155 | "FocusProcess", 156 | { 157 | 0: "AF Not Used", 158 | 1: "AF Used", 159 | }, 160 | ), 161 | 0x0303: ("AFSearch", {0: "Not Ready", 1: "Ready"}), 162 | 0x0304: ("AFAreas", None), 163 | 0x0401: ("FlashExposureCompensation", None), 164 | 0x0500: ( 165 | "WhiteBalance2", 166 | { 167 | 0: "Auto", 168 | 16: "7500K (Fine Weather with Shade)", 169 | 17: "6000K (Cloudy)", 170 | 18: "5300K (Fine Weather)", 171 | 20: "3000K (Tungsten light)", 172 | 21: "3600K (Tungsten light-like)", 173 | 33: "6600K (Daylight fluorescent)", 174 | 34: "4500K (Neutral white fluorescent)", 175 | 35: "4000K (Cool white fluorescent)", 176 | 48: "3600K (Tungsten light-like)", 177 | 256: "Custom WB 1", 178 | 257: "Custom WB 2", 179 | 258: "Custom WB 3", 180 | 259: "Custom WB 4", 181 | 512: "Custom WB 5400K", 182 | 513: "Custom WB 2900K", 183 | 514: "Custom WB 8000K", 184 | }, 185 | ), 186 | 0x0501: ("WhiteBalanceTemperature", None), 187 | 0x0502: ("WhiteBalanceBracket", None), 188 | 0x0503: ("CustomSaturation", None), # (3 numbers: 1. CS Value, 2. Min, 3. Max) 189 | 0x0504: ( 190 | "ModifiedSaturation", 191 | { 192 | 0: "Off", 193 | 1: "CM1 (Red Enhance)", 194 | 2: "CM2 (Green Enhance)", 195 | 3: "CM3 (Blue Enhance)", 196 | 4: "CM4 (Skin Tones)", 197 | }, 198 | ), 199 | 0x0505: ("ContrastSetting", None), # (3 numbers: 1. Contrast, 2. Min, 3. Max) 200 | 0x0506: ("SharpnessSetting", None), # (3 numbers: 1. Sharpness, 2. Min, 3. Max) 201 | 0x0507: ( 202 | "ColorSpace", 203 | { 204 | 0: "sRGB", 205 | 1: "Adobe RGB", 206 | 2: "Pro Photo RGB", 207 | }, 208 | ), 209 | 0x0509: ( 210 | "SceneMode", 211 | { 212 | 0: "Standard", 213 | 6: "Auto", 214 | 7: "Sport", 215 | 8: "Portrait", 216 | 9: "Landscape+Portrait", 217 | 10: "Landscape", 218 | 11: "Night scene", 219 | 13: "Panorama", 220 | 16: "Landscape+Portrait", 221 | 17: "Night+Portrait", 222 | 19: "Fireworks", 223 | 20: "Sunset", 224 | 22: "Macro", 225 | 25: "Documents", 226 | 26: "Museum", 227 | 28: "Beach&Snow", 228 | 30: "Candle", 229 | 35: "Underwater Wide1", 230 | 36: "Underwater Macro", 231 | 39: "High Key", 232 | 40: "Digital Image Stabilization", 233 | 44: "Underwater Wide2", 234 | 45: "Low Key", 235 | 46: "Children", 236 | 48: "Nature Macro", 237 | }, 238 | ), 239 | 0x050A: ( 240 | "NoiseReduction", 241 | { 242 | 0: "Off", 243 | 1: "Noise Reduction", 244 | 2: "Noise Filter", 245 | 3: "Noise Reduction + Noise Filter", 246 | 4: "Noise Filter (ISO Boost)", 247 | 5: "Noise Reduction + Noise Filter (ISO Boost)", 248 | }, 249 | ), 250 | 0x050B: ("DistortionCorrection", {0: "Off", 1: "On"}), 251 | 0x050C: ("ShadingCompensation", {0: "Off", 1: "On"}), 252 | 0x050D: ("CompressionFactor", None), 253 | 0x050F: ( 254 | "Gradation", 255 | { 256 | "-1 -1 1": "Low Key", 257 | "0 -1 1": "Normal", 258 | "1 -1 1": "High Key", 259 | }, 260 | ), 261 | 0x0520: ( 262 | "PictureMode", 263 | { 264 | 1: "Vivid", 265 | 2: "Natural", 266 | 3: "Muted", 267 | 256: "Monotone", 268 | 512: "Sepia", 269 | }, 270 | ), 271 | 0x0521: ("PictureModeSaturation", None), 272 | 0x0522: ("PictureModeHue?", None), 273 | 0x0523: ("PictureModeContrast", None), 274 | 0x0524: ("PictureModeSharpness", None), 275 | 0x0525: ( 276 | "PictureModeBWFilter", 277 | { 278 | 0: "n/a", 279 | 1: "Neutral", 280 | 2: "Yellow", 281 | 3: "Orange", 282 | 4: "Red", 283 | 5: "Green", 284 | }, 285 | ), 286 | 0x0526: ( 287 | "PictureModeTone", 288 | { 289 | 0: "n/a", 290 | 1: "Neutral", 291 | 2: "Sepia", 292 | 3: "Blue", 293 | 4: "Purple", 294 | 5: "Green", 295 | }, 296 | ), 297 | 0x0600: ("Sequence", None), # 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits 298 | 0x0601: ("PanoramaMode", None), # (2 numbers: 1. Mode, 2. Shot number) 299 | 0x0603: ( 300 | "ImageQuality2", 301 | { 302 | 1: "SQ", 303 | 2: "HQ", 304 | 3: "SHQ", 305 | 4: "RAW", 306 | }, 307 | ), 308 | 0x0901: ("ManometerReading", None), 309 | } 310 | -------------------------------------------------------------------------------- /exifread/tags/makernote/sony.py: -------------------------------------------------------------------------------- 1 | """ 2 | Makernote (proprietary) tag definitions for Sony. 3 | 4 | Based on https://www.exiftool.org/TagNames/Sony.html 5 | """ 6 | 7 | from exifread.tags import SubIfdTagDict 8 | 9 | TAGS: SubIfdTagDict = { 10 | # depends on model 11 | # 0x0010: (), 12 | # 0x0020: (), 13 | 0x0102: ( 14 | "Quality", 15 | { 16 | 0: "RAW", 17 | 1: "Super Fine", 18 | 2: "Fine", 19 | 3: "Standard", 20 | 4: "Economy", 21 | 5: "Extra Fine", 22 | 6: "RAW + JPEG/HEIF", 23 | 7: "Compressed RAW", 24 | 8: "Compressed RAW + JPEG", 25 | 9: "Light", 26 | }, 27 | ), 28 | 0x0104: ("FlashExposureComp", None), 29 | 0x0105: ( 30 | "Teleconverter", 31 | { 32 | 0x0: "None", 33 | 0x4: "Minolta/Sony AF 1.4x APO (D) (0x04)", 34 | 0x5: "Minolta/Sony AF 2x APO (D) (0x05)", 35 | 0x48: "Minolta/Sony AF 2x APO (D)", 36 | 0x50: "Minolta AF 2x APO II", 37 | 0x60: "Minolta AF 2x APO", 38 | 0x88: "Minolta/Sony AF 1.4x APO (D)", 39 | 0x90: "Minolta AF 1.4x APO II", 40 | 0xA0: "Minolta AF 1.4x APO", 41 | }, 42 | ), 43 | 0x0112: ("WhiteBalanceFineTune", None), 44 | # depends on model 45 | # 0x0114: (), 46 | 0x0115: ( 47 | "WhiteBalance", 48 | { 49 | 0x0: "Auto", 50 | 0x1: "Color Temperature / Color Filter", 51 | 0x10: "Daylight", 52 | 0x20: "Cloudy", 53 | 0x30: "Shade", 54 | 0x40: "Tungsten", 55 | 0x50: "Flash", 56 | 0x60: "Fluorescent", 57 | 0x70: "Custom", 58 | 0x80: "Underwater", 59 | }, 60 | ), 61 | # depends on model 62 | # 0x0116: (), 63 | 0x0E00: ("PrintIM", None), 64 | 0x1000: ("MultiBurstMode", {0: "Off", 1: "On"}), 65 | 0x1001: ("MultiBurstImageWidth", None), 66 | 0x1002: ("MultiBurstImageHeight", None), 67 | # needs specific processing? 68 | # 0x1003: ("Panorama", {}), 69 | 0x2001: ("PreviewImage", None), 70 | 0x2002: ("Rating", None), 71 | 0x2004: ("Contrast", None), 72 | 0x2005: ("Saturation", None), 73 | 0x2006: ("Sharpness", None), 74 | 0x2007: ("Brightness", None), 75 | 0x2008: ( 76 | "LongExposureNoiseReduction", 77 | { 78 | 0x0: "Off", 79 | 0x1: "On (unused)", 80 | 0x10001: "On (dark subtracted)", 81 | 0xFFFF0000: "Off (65535)", 82 | 0xFFFF0001: "On (65535)", 83 | 0xFFFFFFFF: "N/A", 84 | }, 85 | ), 86 | 0x2009: ( 87 | "HighISONoiseReduction", 88 | { 89 | 0: "Off", 90 | 1: "Low", 91 | 2: "Normal", 92 | 3: "High", 93 | 256: "Auto", 94 | 65535: "N/A", 95 | }, 96 | ), 97 | # Needs specific processing? 98 | # 0x200A: ("HDR", {}), 99 | 0x200B: ( 100 | "MultiFrameNoiseReduction", 101 | { 102 | 0: "Off", 103 | 1: "On", 104 | 255: "N/A", 105 | }, 106 | ), 107 | 0x200E: ( 108 | "PictureEffect", 109 | { 110 | 0: "Off", 111 | 1: "Toy Camera", 112 | 2: "Pop Color", 113 | 3: "Posterization", 114 | 4: "Posterization B/W", 115 | 5: "Retro Photo", 116 | 6: "Soft High Key", 117 | 7: "Partial Color (red)", 118 | 8: "Partial Color (green)", 119 | 9: "Partial Color (blue)", 120 | 10: "Partial Color (yellow)", 121 | 13: "High Contrast Monochrome", 122 | 16: "Toy Camera (normal)", 123 | 17: "Toy Camera (cool)", 124 | 18: "Toy Camera (warm)", 125 | 19: "Toy Camera (green)", 126 | 20: "Toy Camera (magenta)", 127 | 32: "Soft Focus (low)", 128 | 33: "Soft Focus", 129 | 34: "Soft Focus (high)", 130 | 48: "Miniature (auto)", 131 | 49: "Miniature (top)", 132 | 50: "Miniature (middle horizontal)", 133 | 51: "Miniature (bottom)", 134 | 52: "Miniature (left)", 135 | 53: "Miniature (middle vertical)", 136 | 54: "Miniature (right)", 137 | 64: "HDR Painting (low)", 138 | 65: "HDR Painting", 139 | 66: "HDR Painting (high)", 140 | 80: "Rich-tone Monochrome", 141 | 97: "Water Color", 142 | 98: "Water Color 2", 143 | 112: "Illustration (low)", 144 | 113: "Illustration", 145 | 114: "Illustration (high)", 146 | }, 147 | ), 148 | 0x200F: ( 149 | "SoftSkinEffect", 150 | { 151 | 0: "Off", 152 | 1: "Low", 153 | 2: "Mid", 154 | 3: "High", 155 | 4294967295: "N/A", 156 | }, 157 | ), 158 | # depends on model 159 | # 0x2010: (), 160 | 0x2011: ( 161 | "VignettingCorrection", 162 | { 163 | 0: "Off", 164 | 2: "Auto", 165 | 4294967295: "N/A", 166 | }, 167 | ), 168 | 0x2012: ( 169 | "LateralChromaticAberration", 170 | { 171 | 0: "Off", 172 | 2: "Auto", 173 | 4294967295: "N/A", 174 | }, 175 | ), 176 | 0x2013: ( 177 | "DistortionCorrectionSetting", 178 | { 179 | 0: "Off", 180 | 2: "Auto", 181 | 4294967295: "N/A", 182 | }, 183 | ), 184 | 0x2014: ("WBShiftAB_GM", None), 185 | 0x2016: ("AutoPortraitFramed", {0: "No", 1: "Yes"}), 186 | 0x2017: ( 187 | "FlashAction", 188 | { 189 | 0: "Did not fire", 190 | 1: "Flash Fired", 191 | 2: "External Flash Fired", 192 | 3: "Wireless Controlled Flash Fired", 193 | }, 194 | ), 195 | 0x201A: ("ElectronicFrontCurtainShutter", {0: "Off", 1: "On"}), 196 | 0x201B: ( 197 | "FocusMode", 198 | { 199 | 0: "Manual", 200 | 2: "AF-S", 201 | 3: "AF-C", 202 | 4: "AF-A", 203 | 6: "DMF", 204 | 7: "AF-D", 205 | }, 206 | ), 207 | 0x2021: ( 208 | "FlashExposureComp", 209 | { 210 | 0: "Off", 211 | 1: "Face tracking", 212 | 2: "Lock On AF", 213 | }, 214 | ), 215 | 0xB001: ( 216 | "SonyModelID", 217 | { 218 | 2: "DSC-R1", 219 | 256: "DSLR-A100", 220 | 257: "DSLR-A900", 221 | 258: "DSLR-A700", 222 | 259: "DSLR-A200", 223 | 260: "DSLR-A350", 224 | 261: "DSLR-A300", 225 | 262: "DSLR-A900 (APS-C mode)", 226 | 263: "DSLR-A380/A390", 227 | 264: "DSLR-A330", 228 | 265: "DSLR-A230", 229 | 266: "DSLR-A290", 230 | 269: "DSLR-A850", 231 | 270: "DSLR-A850 (APS-C mode)", 232 | 273: "DSLR-A550", 233 | 274: "DSLR-A500", 234 | 275: "DSLR-A450", 235 | 278: "NEX-5", 236 | 279: "NEX-3", 237 | 280: "SLT-A33", 238 | 281: "SLT-A55 / SLT-A55V", 239 | 282: "DSLR-A560", 240 | 283: "DSLR-A580", 241 | 284: "NEX-C3", 242 | 285: "SLT-A35", 243 | 286: "SLT-A65 / SLT-A65V", 244 | 287: "SLT-A77 / SLT-A77V", 245 | 288: "NEX-5N", 246 | 289: "NEX-7", 247 | 290: "NEX-VG20E", 248 | 291: "SLT-A37", 249 | 292: "SLT-A57", 250 | 293: "NEX-F3", 251 | 294: "SLT-A99 / SLT-A99V", 252 | 295: "NEX-6", 253 | 296: "NEX-5R", 254 | 297: "DSC-RX100", 255 | 298: "DSC-RX1", 256 | 299: "NEX-VG900", 257 | 300: "NEX-VG30E", 258 | 302: "ILCE-3000 / ILCE-3500", 259 | 303: "SLT-A58", 260 | 305: "NEX-3N", 261 | 306: "ILCE-7", 262 | 307: "NEX-5T", 263 | 308: "DSC-RX100M2", 264 | 309: "DSC-RX10", 265 | 310: "DSC-RX1R", 266 | 311: "ILCE-7R", 267 | 312: "ILCE-6000", 268 | 313: "ILCE-5000", 269 | 317: "DSC-RX100M3", 270 | 318: "ILCE-7S", 271 | 319: "ILCA-77M2", 272 | 339: "ILCE-5100", 273 | 340: "ILCE-7M2", 274 | 341: "DSC-RX100M4", 275 | 342: "DSC-RX10M2", 276 | 344: "DSC-RX1RM2", 277 | 346: "ILCE-QX1", 278 | 347: "ILCE-7RM2", 279 | 350: "ILCE-7SM2", 280 | 353: "ILCA-68", 281 | 354: "ILCA-99M2", 282 | 355: "DSC-RX10M3", 283 | 356: "DSC-RX100M5", 284 | 357: "ILCE-6300", 285 | 358: "ILCE-9", 286 | 360: "ILCE-6500", 287 | 362: "ILCE-7RM3", 288 | 363: "ILCE-7M3", 289 | 364: "DSC-RX0", 290 | 365: "DSC-RX10M4", 291 | 366: "DSC-RX100M6", 292 | 367: "DSC-HX99", 293 | 369: "DSC-RX100M5A", 294 | 371: "ILCE-6400", 295 | 372: "DSC-RX0M2", 296 | 373: "DSC-HX95", 297 | 374: "DSC-RX100M7", 298 | 375: "ILCE-7RM4", 299 | 376: "ILCE-9M2", 300 | 378: "ILCE-6600", 301 | 379: "ILCE-6100", 302 | 380: "ZV-1", 303 | 381: "ILCE-7C", 304 | 382: "ZV-E10", 305 | 383: "ILCE-7SM3", 306 | 384: "ILCE-1", 307 | 385: "ILME-FX3", 308 | 386: "ILCE-7RM3A", 309 | 387: "ILCE-7RM4A", 310 | 388: "ILCE-7M4", 311 | 389: "ZV-1F", 312 | 390: "ILCE-7RM5", 313 | 391: "ILME-FX30", 314 | 392: "ILCE-9M3", 315 | 393: "ZV-E1", 316 | 394: "ILCE-6700", 317 | 395: "ZV-1M2", 318 | 396: "ILCE-7CR", 319 | 397: "ILCE-7CM2", 320 | 398: "ILX-LR1", 321 | 399: "ZV-E10M2", 322 | 400: "ILCE-1M2", 323 | }, 324 | ), 325 | 0xB020: ("CreativeStyle", None), 326 | 0xB021: ("ColorTemperature", None), 327 | 0xB022: ("ColorCompensationFilter", None), 328 | 0xB023: ( 329 | "SceneMode", 330 | { 331 | 0: "Standard", 332 | 1: "Portrait", 333 | 2: "Text", 334 | 3: "Night Scene", 335 | 4: "Sunset", 336 | 5: "Sports", 337 | 6: "Landscape", 338 | 7: "Night Portrait", 339 | 8: "Macro", 340 | 9: "Super Macro", 341 | 16: "Auto", 342 | 17: "Night View / Portrait", 343 | 18: "Sweep Panorama", 344 | 19: "Handheld Night Shot", 345 | 20: "Anti Motion Blur", 346 | 21: "Cont. Priority AE", 347 | 22: "Auto+", 348 | 23: "3D Sweep Panorama", 349 | 24: "Superior Auto", 350 | 25: "High Sensitivity", 351 | 26: "Fireworks", 352 | 27: "Food", 353 | 28: "Pet", 354 | 33: "HDR", 355 | }, 356 | ), 357 | 0xB024: ( 358 | "ZoneMatching", 359 | { 360 | 0: "ISO Setting Used", 361 | 1: "High Key", 362 | 2: "Low Key", 363 | }, 364 | ), 365 | 0xB025: ( 366 | "DynamicRangeOptimizer", 367 | { 368 | 0: "Off", 369 | 1: "Standard", 370 | 2: "Advanced Auto", 371 | 3: "Auto", 372 | 8: "Advanced Lv1", 373 | 9: "Advanced Lv2", 374 | 10: "Advanced Lv3", 375 | 11: "Advanced Lv4", 376 | 12: "Advanced Lv5", 377 | 16: "Lv1", 378 | 17: "Lv2", 379 | 18: "Lv3", 380 | 19: "Lv4", 381 | 20: "Lv5", 382 | }, 383 | ), 384 | 0xB026: ( 385 | "ImageStabilization", 386 | { 387 | 0: "Off", 388 | 1: "On", 389 | 4294967295: "N/A", 390 | }, 391 | ), 392 | # TODO 393 | # 0xB027: ("LensType", {}), 394 | 0xB029: ( 395 | "ColorMode", 396 | { 397 | 0: "Standard", 398 | 1: "Vivid", 399 | 2: "Portrait", 400 | 3: "Landscape", 401 | 4: "Sunset", 402 | 5: "Night View / Portrait", 403 | 6: "B & W", 404 | 7: "Adobe RGB", 405 | 12: "Neutral", 406 | 13: "Clear", 407 | 14: "Deep", 408 | 15: "Light", 409 | 16: "Autumn Leaves", 410 | 17: "Sepia", 411 | 18: "FL", 412 | 19: "Vivid 2", 413 | 20: "IN", 414 | 21: "SH", 415 | 100: "Neutral", 416 | 101: "Clear", 417 | 102: "Deep", 418 | 103: "Light", 419 | 104: "Night View", 420 | 105: "Autumn Leaves", 421 | 255: "Off", 422 | 4294967295: "N/A", 423 | }, 424 | ), 425 | 0xB040: ( 426 | "Macro", 427 | { 428 | 0: "Off", 429 | 1: "On", 430 | 2: "Close Focus", 431 | 65535: "N/A", 432 | }, 433 | ), 434 | 0xB041: ( 435 | "ExposureMode", 436 | { 437 | 0: "Program AE", 438 | 1: "Portrait", 439 | 2: "Beach", 440 | 3: "Sports", 441 | 4: "Snow", 442 | 5: "Landscape", 443 | 6: "Auto", 444 | 7: "Aperture-priority AE", 445 | 8: "Shutter speed priority AE", 446 | 9: "Night Scene / Twilight", 447 | 10: "Hi-Speed Shutter", 448 | 11: "Twilight Portrait", 449 | 12: "Soft Snap/Portrait", 450 | 13: "Fireworks", 451 | 14: "Smile Shutter", 452 | 15: "Manual", 453 | 18: "High Sensitivity", 454 | 19: "Macro", 455 | 20: "Advanced Sports Shooting", 456 | 29: "Underwater", 457 | 33: "Food", 458 | 34: "Sweep Panorama", 459 | 35: "Handheld Night Shot", 460 | 36: "Anti Motion Blur", 461 | 37: "Pet", 462 | 38: "Backlight Correction HDR", 463 | 39: "Superior Auto", 464 | 40: "Background Defocus", 465 | 41: "Soft Skin", 466 | 42: "3D Image", 467 | 65535: "N/A", 468 | }, 469 | ), 470 | 0xB042: ( 471 | "FocusMode", 472 | { 473 | 1: "AF-S", 474 | 2: "AF-C", 475 | 4: "Permanent-AF", 476 | 65535: "N/A", 477 | }, 478 | ), 479 | # depends on model 480 | # 0xb043: (), 481 | 0xB044: ( 482 | "AFIlluminator", 483 | { 484 | 0: "Off", 485 | 1: "On", 486 | 65535: "N/A", 487 | }, 488 | ), 489 | 0xB047: ( 490 | "JPEGQuality", 491 | { 492 | 0: "Standard", 493 | 1: "Fine", 494 | 2: "Extra Fine", 495 | 65535: "N/A", 496 | }, 497 | ), 498 | 0xB048: ( 499 | "FlashLevel", 500 | { 501 | -32768: "Low", 502 | -9: "-9/3", 503 | -8: "-8/3", 504 | -7: "-7/3", 505 | -6: "-6/3", 506 | -5: "-5/3", 507 | -4: "-4/3", 508 | -3: "-3/3", 509 | -2: "-2/3", 510 | -1: "-1/3", 511 | 0: "Normal", 512 | 1: "+1/3", 513 | 2: "+2/3", 514 | 3: "+3/3", 515 | 4: "+4/3", 516 | 5: "+5/3", 517 | 6: "+6/3", 518 | 9: "+9/3", 519 | 128: "N/A", 520 | 32767: "High", 521 | }, 522 | ), 523 | 0xB049: ( 524 | "ReleaseMode", 525 | { 526 | 0: "Normal", 527 | 2: "Continuous", 528 | 5: " Exposure Bracketing", 529 | 6: "White Balance Bracketing", 530 | 8: "DRO Bracketing", 531 | 65535: "N/A", 532 | }, 533 | ), 534 | 0xB04A: ( 535 | "SequenceNumber", 536 | { 537 | 0: "Single", 538 | 65535: "N/A", 539 | }, 540 | ), 541 | 0xB04B: ( 542 | "Anti-Blur", 543 | { 544 | 0: "Off", 545 | 1: "On - Continuous", 546 | 2: "On - Shooting", 547 | 65535: "N/A", 548 | }, 549 | ), 550 | 0xB04E: ( 551 | "FocusMode", 552 | { 553 | 0: "Manual", 554 | 2: "AF-S", 555 | 3: "AF-C", 556 | 5: "Semi-manual", 557 | 6: "DMF", 558 | }, 559 | ), 560 | 0xB04F: ( 561 | "DynamicRangeOptimizer", 562 | { 563 | 0: "Off", 564 | 1: "Standard", 565 | 2: "Plus", 566 | }, 567 | ), 568 | 0xB050: ( 569 | "HighISONoiseReduction2", 570 | { 571 | 0: "Normal", 572 | 1: "High", 573 | 2: "Low", 574 | 3: "Off", 575 | 65535: "N/A", 576 | }, 577 | ), 578 | 0xB052: ( 579 | "IntelligentAuto", 580 | { 581 | 0: "Off", 582 | 1: "On", 583 | 2: "Advanced", 584 | }, 585 | ), 586 | 0xB054: ( 587 | "WhiteBalance", 588 | { 589 | 0: "Auto", 590 | 4: "Custom", 591 | 5: "Daylight", 592 | 6: "Cloudy", 593 | 7: "Cool White Fluorescent", 594 | 8: "Day White Fluorescent", 595 | 9: "Daylight Fluorescent", 596 | 10: "Incandescent2", 597 | 11: "Warm White Fluorescent", 598 | 14: "Incandescent", 599 | 15: "Flash", 600 | 17: "Underwater 1 (Blue Water)", 601 | 18: "Underwater 2 (Green Water)", 602 | 19: "Underwater Auto", 603 | }, 604 | ), 605 | } 606 | -------------------------------------------------------------------------------- /exifread/tags/str_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Misc utilities. 3 | """ 4 | 5 | 6 | def make_string_uc(seq) -> str: 7 | """ 8 | Special version to deal with the code in the first 8 bytes of a user comment. 9 | First 8 bytes gives coding system e.g. ASCII vs. JIS vs Unicode. 10 | """ 11 | if not isinstance(seq, str): 12 | # Remove code from sequence only if it is valid 13 | if make_string(seq[:8]).upper() in ("ASCII", "UNICODE", "JIS", ""): 14 | seq = seq[8:] 15 | # Of course, this is only correct if ASCII, and the standard explicitly 16 | # allows JIS and Unicode. 17 | return make_string(seq) 18 | 19 | 20 | def make_string(seq) -> str: 21 | """ 22 | Don't throw an exception when given an out of range character. 23 | """ 24 | string = "" 25 | for char in seq: 26 | # Screen out non-printing characters 27 | try: 28 | if 32 <= char < 256: 29 | string += chr(char) 30 | except TypeError: 31 | pass 32 | 33 | # If no printing chars 34 | if not string: 35 | if isinstance(seq, list): 36 | string = "".join(map(str, seq)) 37 | # Some UserComment lists only contain null bytes, nothing valuable to return 38 | if set(string) == {"0"}: 39 | return "" 40 | else: 41 | string = str(seq) 42 | 43 | # Clean undesirable characters on any end 44 | return string.strip(" \x00") 45 | -------------------------------------------------------------------------------- /exifread/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Misc utilities. 3 | """ 4 | 5 | from fractions import Fraction 6 | from typing import Optional, Tuple 7 | 8 | 9 | def _degrees_to_decimal(degrees: float, minutes: float, seconds: float) -> float: 10 | """ 11 | Converts coordinates from a degrees minutes seconds format to a decimal degrees format. 12 | Reference: https://en.wikipedia.org/wiki/Geographic_coordinate_conversion 13 | """ 14 | return degrees + minutes / 60 + seconds / 3600 15 | 16 | 17 | def get_gps_coords(tags: dict) -> Optional[Tuple[float, float]]: 18 | """ 19 | Extract tuple of latitude and longitude values in decimal degrees format from EXIF tags. 20 | Return None if no GPS coordinates are found. 21 | Handles regular and serialized Exif tags. 22 | """ 23 | gps = { 24 | "lat_coord": "GPS GPSLatitude", 25 | "lat_ref": "GPS GPSLatitudeRef", 26 | "lng_coord": "GPS GPSLongitude", 27 | "lng_ref": "GPS GPSLongitudeRef", 28 | } 29 | 30 | # Verify if required keys are a subset of provided tags 31 | if not set(gps.values()) <= tags.keys(): 32 | return None 33 | 34 | # If tags have not been converted to native Python types, do it 35 | if not isinstance(tags[gps["lat_coord"]], list): 36 | tags[gps["lat_coord"]] = [c.decimal() for c in tags[gps["lat_coord"]].values] 37 | tags[gps["lng_coord"]] = [c.decimal() for c in tags[gps["lng_coord"]].values] 38 | tags[gps["lat_ref"]] = tags[gps["lat_ref"]].values 39 | tags[gps["lng_ref"]] = tags[gps["lng_ref"]].values 40 | 41 | lat = _degrees_to_decimal(*tags[gps["lat_coord"]]) 42 | if tags[gps["lat_ref"]] == "S": 43 | lat *= -1 44 | 45 | lng = _degrees_to_decimal(*tags[gps["lng_coord"]]) 46 | if tags[gps["lng_ref"]] == "W": 47 | lng *= -1 48 | 49 | return lat, lng 50 | 51 | 52 | class Ratio(Fraction): 53 | """ 54 | Ratio object that eventually will be able to reduce itself to lowest 55 | common denominator for printing. 56 | """ 57 | 58 | _numerator: Optional[int] 59 | _denominator: Optional[int] 60 | 61 | # We're immutable, so use __new__ not __init__ 62 | def __new__(cls, numerator: int = 0, denominator: Optional[int] = None): 63 | try: 64 | self = super(Ratio, cls).__new__(cls, numerator, denominator) 65 | except ZeroDivisionError: 66 | self = super(Ratio, cls).__new__(cls) 67 | self._numerator = numerator 68 | self._denominator = denominator 69 | return self 70 | 71 | def __repr__(self) -> str: 72 | return str(self) 73 | 74 | @property 75 | def num(self) -> int: 76 | return self.numerator 77 | 78 | @property 79 | def den(self) -> int: 80 | return self.denominator 81 | 82 | def decimal(self) -> float: 83 | return float(self) 84 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 68.0.0", "wheel >= 0.41.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | 6 | [project] 7 | name = "ExifRead" 8 | description = "Library to extract Exif information from digital camera image files." 9 | readme = "README.rst" 10 | license = {file = "LICENSE"} 11 | requires-python = ">= 3.7" 12 | classifiers = [ 13 | "Development Status :: 5 - Production/Stable", 14 | "Environment :: Console", 15 | "Intended Audience :: Developers", 16 | "Intended Audience :: End Users/Desktop", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3 :: Only", 20 | "Topic :: Utilities", 21 | ] 22 | keywords = ["exif", "image", "metadata", "photo"] 23 | authors = [ 24 | {name = "Ianaré Sévi"}, 25 | ] 26 | dependencies = [] 27 | dynamic = ["version"] 28 | 29 | 30 | [project.optional-dependencies] 31 | dev = [ 32 | "pre-commit~=2.21", 33 | "pylint~=3.1", 34 | "build~=1.0", 35 | ] 36 | test = [ 37 | "pytest~=7.4", 38 | ] 39 | 40 | 41 | [project.urls] 42 | Repository = "https://github.com/ianare/exif-py" 43 | Changelog = "https://github.com/ianare/exif-py/blob/master/ChangeLog.rst" 44 | 45 | 46 | [project.scripts] 47 | "EXIF.py" = "exifread.cli:main" 48 | 49 | 50 | [tool.setuptools.packages.find] 51 | include = ["exifread*"] 52 | exclude = ["tests"] 53 | 54 | 55 | [tool.setuptools.dynamic] 56 | version = {attr = "exifread.__version__"} 57 | 58 | 59 | [tool.setuptools.package-data] 60 | "exifread" = ["py.typed"] 61 | 62 | 63 | [tool.mypy] 64 | disallow_any_unimported = true 65 | disallow_subclassing_any = true 66 | disallow_untyped_calls = true 67 | disallow_untyped_decorators = true 68 | # disallow_untyped_defs = true 69 | no_implicit_optional = true 70 | strict_equality = true 71 | warn_no_return = true 72 | # warn_return_any = true 73 | warn_unused_ignores = true 74 | warn_unreachable = true 75 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/__init__.py -------------------------------------------------------------------------------- /tests/resources/README.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Exif Samples 3 | ============ 4 | 5 | Sample images for testing Exif metadata retrieval. 6 | 7 | Adding Images 8 | ============= 9 | 10 | Please do! 11 | 12 | User-contributed images will be released under the **Attribution-ShareAlike 4.0 International** license. 13 | -------------------------------------------------------------------------------- /tests/resources/avif/mountains.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/avif/mountains.avif -------------------------------------------------------------------------------- /tests/resources/heic/heic_hdlr_box.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/heic/heic_hdlr_box.jpg -------------------------------------------------------------------------------- /tests/resources/heic/mobile/HMD_Nokia_8.3_5G.heif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/heic/mobile/HMD_Nokia_8.3_5G.heif -------------------------------------------------------------------------------- /tests/resources/heic/mobile/HMD_Nokia_8.3_5G_hdr.heif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/heic/mobile/HMD_Nokia_8.3_5G_hdr.heif -------------------------------------------------------------------------------- /tests/resources/heic/mobile/iphone_13_pro_max.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/heic/mobile/iphone_13_pro_max.heic -------------------------------------------------------------------------------- /tests/resources/heic/samplefilehub.heif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/heic/samplefilehub.heif -------------------------------------------------------------------------------- /tests/resources/heic/spring_1440x960.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/heic/spring_1440x960.heic -------------------------------------------------------------------------------- /tests/resources/jpg/Canon_40D.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Canon_40D.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Canon_40D_photoshop_import.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Canon_40D_photoshop_import.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Canon_DIGITAL_IXUS_400.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Canon_DIGITAL_IXUS_400.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Canon_PowerShot_S40.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Canon_PowerShot_S40.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Fujifilm_FinePix6900ZOOM.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Fujifilm_FinePix6900ZOOM.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Fujifilm_FinePix_E500.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Fujifilm_FinePix_E500.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Kodak_CX7530.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Kodak_CX7530.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Konica_Minolta_DiMAGE_Z3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Konica_Minolta_DiMAGE_Z3.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Nikon_COOLPIX_P1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Nikon_COOLPIX_P1.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Nikon_D70.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Nikon_D70.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Olympus_C8080WZ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Olympus_C8080WZ.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/PaintTool_sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/PaintTool_sample.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Panasonic_DMC-FZ30.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Panasonic_DMC-FZ30.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Pentax_K10D.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Pentax_K10D.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/README.txt: -------------------------------------------------------------------------------- 1 | 2 | |== INFO ON TEST IMAGES ==| 3 | 4 | Unless stated otherwise, all images come from Wikipedia Commons - http://commons.wikimedia.org 5 | For author and license information please refer to their respective description pages. 6 | 7 | All images were scaled down using the GIMP v 2.4.5 since all we care about is the Exif info. 8 | Tested before and after scaling to ensure Exif data was not modified (except for 'software' field). 9 | 10 | 11 | == Filename == == Wiki Filename == 12 | 13 | = Camera Makes and Models = 14 | Canon_40D.jpg Iguana_iguana_male_head.jpg 15 | Canon_40D_photoshop_import.jpg Anolis_equestris_-_bright_close_3-4.jpg 16 | Canon_DIGITAL_IXUS_400.jpg Ducati749.jpg 17 | Fujifilm_FinePix6900ZOOM.jpg Hylidae_cinerea.JPG 18 | Fujifilm_FinePix_E500.jpg VlaamseGaaiVeertje1480.JPG 19 | Kodak_CX7530.jpg Red-headed_Rock_Agama.jpg 20 | Konica_Minolta_DiMAGE_Z3.jpg Knechtova01.jpg 21 | Nikon_COOLPIX_P1.jpg Miyagikotsu-castle6861.JPG 22 | Nikon_D70.jpg Anolis_carolinensis_brown.jpg 23 | Olympus_C8080WZ.jpg Pterois_volitans_Manado-e.jpg 24 | Panasonic_DMC-FZ30.jpg Rømø_-_St.Klement_-_Kanzel_3.jpg 25 | Pentax_K10D.jpg Mrs._Herbert_Stevens_May_2008.jpg 26 | Ricoh_Caplio_RR330.jpg Steveston_dusk.JPG 27 | Samsung_Digimax_i50_MP3.jpg Villa_di_Poggio_a_Caiano,_sala_neoclassica_4.JPG 28 | Sony_HDR-HC3.jpg Positive_roll_film.jpg 29 | WWL_(Polaroid)_ION230.jpg PoudriereBoisSousRoche3.jpg 30 | Sony_DSLR-A200.jpg Aloe_Vera_Flower.jpg 31 | 32 | = Other Stuff = 33 | long_description.jpg US_10th_Mountain_Division_soldiers_in_Afghanistan.jpg 34 | 35 | 36 | 37 | Contributions: 38 | 39 | PaintTool_sample.jpg -- Submitted by Jan Trofimov (OXIj) 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/resources/jpg/Reconyx_HC500_Hyperfire.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Reconyx_HC500_Hyperfire.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Ricoh_Caplio_RR330.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Ricoh_Caplio_RR330.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Samsung_Digimax_i50_MP3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Samsung_Digimax_i50_MP3.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Sony_DSLR-A200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Sony_DSLR-A200.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Sony_HDR-HC3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Sony_HDR-HC3.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/Sony_alpha_a58.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/Sony_alpha_a58.JPG -------------------------------------------------------------------------------- /tests/resources/jpg/WWL_Polaroid_ION230.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/WWL_Polaroid_ION230.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/corrupted.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/corrupted.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/README.txt: -------------------------------------------------------------------------------- 1 | 2 | Images from exif.org 3 | 4 | http://www.exif.org/samples.html 5 | 6 | -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/canon-ixus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/canon-ixus.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/fujifilm-dx10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/fujifilm-dx10.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/fujifilm-finepix40i.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/fujifilm-finepix40i.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/fujifilm-mx1700.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/fujifilm-mx1700.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/kodak-dc210.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/kodak-dc210.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/kodak-dc240.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/kodak-dc240.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/nikon-e950.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/nikon-e950.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/olympus-c960.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/olympus-c960.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/olympus-d320l.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/olympus-d320l.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/ricoh-rdc5300.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/ricoh-rdc5300.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/sanyo-vpcg250.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/sanyo-vpcg250.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/sanyo-vpcsx550.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/sanyo-vpcsx550.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/sony-cybershot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/sony-cybershot.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/sony-d700.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/sony-d700.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/exif-org/sony-powershota5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/exif-org/sony-powershota5.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/gps/DSCN0010.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/gps/DSCN0010.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/gps/DSCN0012.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/gps/DSCN0012.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/gps/DSCN0021.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/gps/DSCN0021.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/gps/DSCN0025.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/gps/DSCN0025.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/gps/DSCN0027.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/gps/DSCN0027.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/gps/DSCN0029.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/gps/DSCN0029.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/gps/DSCN0038.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/gps/DSCN0038.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/gps/DSCN0040.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/gps/DSCN0040.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/gps/DSCN0042.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/gps/DSCN0042.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/gps/README.txt: -------------------------------------------------------------------------------- 1 | 2 | Images having GPS Exif info 3 | 4 | http://www.gaia-gis.it/spatialite-2.3.1/resources.html 5 | -------------------------------------------------------------------------------- /tests/resources/jpg/hdr/README.txt: -------------------------------------------------------------------------------- 1 | 2 | Images for testing HDR 3 | 4 | Contributed by: Jesus Cea 5 | -------------------------------------------------------------------------------- /tests/resources/jpg/hdr/canon_hdr_NO.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/hdr/canon_hdr_NO.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/hdr/canon_hdr_YES.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/hdr/canon_hdr_YES.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/hdr/iphone_hdr_NO.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/hdr/iphone_hdr_NO.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/hdr/iphone_hdr_YES.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/hdr/iphone_hdr_YES.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/invalid/README.txt: -------------------------------------------------------------------------------- 1 | Invalid / broken images. 2 | 3 | The script should not go into an endless loop trying to read these. 4 | -------------------------------------------------------------------------------- /tests/resources/jpg/invalid/image00971.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/invalid/image00971.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/invalid/image01088.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/invalid/image01088.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/invalid/image01137.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/invalid/image01137.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/invalid/image01551.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/invalid/image01551.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/invalid/image01713.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/invalid/image01713.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/invalid/image01980.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/invalid/image01980.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/invalid/image02206.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/invalid/image02206.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/long_description.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/long_description.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/mobile/HMD_Nokia_8.3_5G.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/mobile/HMD_Nokia_8.3_5G.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/mobile/HMD_Nokia_8.3_5G_hdr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/mobile/HMD_Nokia_8.3_5G_hdr.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/mobile/jolla.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/mobile/jolla.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/README.txt: -------------------------------------------------------------------------------- 1 | Images to test proper reading of orientation tags. 2 | -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/landscape_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/landscape_1.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/landscape_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/landscape_2.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/landscape_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/landscape_3.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/landscape_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/landscape_4.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/landscape_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/landscape_5.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/landscape_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/landscape_6.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/landscape_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/landscape_7.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/landscape_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/landscape_8.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/portrait_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/portrait_1.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/portrait_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/portrait_2.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/portrait_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/portrait_3.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/portrait_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/portrait_4.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/portrait_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/portrait_5.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/portrait_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/portrait_6.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/portrait_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/portrait_7.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/orientation/portrait_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/orientation/portrait_8.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/11-tests.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/11-tests.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/22-canon_tags.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/22-canon_tags.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/28-hex_value.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/28-hex_value.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/30-type_error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/30-type_error.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/32-lens_data.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/32-lens_data.jpeg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/33-type_error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/33-type_error.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/35-empty.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/35-empty.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/36-memory_error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/36-memory_error.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/42_IndexError.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/42_IndexError.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/45-gps_ifd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/45-gps_ifd.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/46_UnicodeEncodeError.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/46_UnicodeEncodeError.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/67-0_length_string.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/67-0_length_string.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/87_OSError.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/87_OSError.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/Xiaomi_Mi_9T_KeyError.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/Xiaomi_Mi_9T_KeyError.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/tests/nikon_D3100_TypeError.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/tests/nikon_D3100_TypeError.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/xmp/BlueSquare.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/xmp/BlueSquare.jpg -------------------------------------------------------------------------------- /tests/resources/jpg/xmp/no_exif.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jpg/xmp/no_exif.jpg -------------------------------------------------------------------------------- /tests/resources/jxl/test_0001.jxl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/jxl/test_0001.jxl -------------------------------------------------------------------------------- /tests/resources/raw/nikon_z_9_high_efficiency_compressed_dx_cropped_max_overexposed.dng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/raw/nikon_z_9_high_efficiency_compressed_dx_cropped_max_overexposed.dng -------------------------------------------------------------------------------- /tests/resources/raw/nikon_z_9_high_efficiency_compressed_dx_cropped_max_overexposed.nef: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/raw/nikon_z_9_high_efficiency_compressed_dx_cropped_max_overexposed.nef -------------------------------------------------------------------------------- /tests/resources/raw/sony_alpha_a7iii_raw_image.ARW: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/raw/sony_alpha_a7iii_raw_image.ARW -------------------------------------------------------------------------------- /tests/resources/tiff/Arbitro.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/tiff/Arbitro.tiff -------------------------------------------------------------------------------- /tests/resources/tiff/BSG1.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/tiff/BSG1.tiff -------------------------------------------------------------------------------- /tests/resources/tiff/Crémieux11.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/tiff/Crémieux11.tiff -------------------------------------------------------------------------------- /tests/resources/tiff/DudleyLeavittUtah.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/tiff/DudleyLeavittUtah.tiff -------------------------------------------------------------------------------- /tests/resources/tiff/Jobagent.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/tiff/Jobagent.tiff -------------------------------------------------------------------------------- /tests/resources/tiff/Picoawards.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/tiff/Picoawards.tiff -------------------------------------------------------------------------------- /tests/resources/tiff/README.txt: -------------------------------------------------------------------------------- 1 | 2 | |== INFO ON TEST IMAGES ==| 3 | 4 | Unless stated otherwise, all images come from Wikipedia Commons - http://commons.wikimedia.org 5 | For author and license information please refer to their respective description pages. -------------------------------------------------------------------------------- /tests/resources/tiff/Rudless.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/tiff/Rudless.tiff -------------------------------------------------------------------------------- /tests/resources/tiff/Tless0.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianare/exif-py/f7c2a42b324b0472f73648f380003d5d90cf0c5c/tests/resources/tiff/Tless0.tiff -------------------------------------------------------------------------------- /tests/test_process_file.py: -------------------------------------------------------------------------------- 1 | """Basic tests.""" 2 | 3 | import logging 4 | from pathlib import Path 5 | 6 | import pytest 7 | 8 | import exifread 9 | from exifread import DEFAULT_STOP_TAG 10 | 11 | RESOURCES_ROOT = Path(__file__).parent / "resources" 12 | 13 | 14 | def test_corrupted_exception(): 15 | file_path = RESOURCES_ROOT / "jpg/corrupted.jpg" 16 | with open(file_path, "rb") as fh: 17 | with pytest.raises(ValueError) as err: 18 | exifread.process_file(fh=fh, strict=True) 19 | assert "tag 0x089C" in str(err.value) 20 | 21 | 22 | def test_corrupted_pass(): 23 | file_path = RESOURCES_ROOT / "jpg/corrupted.jpg" 24 | with open(file_path, "rb") as fh: 25 | tags = exifread.process_file(fh=fh, strict=False) 26 | assert "EXIF Contrast" in tags 27 | assert len(tags) == 69 28 | 29 | 30 | @pytest.mark.parametrize("builtin_types", (True, False)) 31 | @pytest.mark.parametrize( 32 | "stop_tag, tag_count", (("ColorSpace", 39), (DEFAULT_STOP_TAG, 51)) 33 | ) 34 | def test_stop_at_tag(builtin_types, stop_tag, tag_count): 35 | file_path = RESOURCES_ROOT / "jpg/Canon_40D.jpg" 36 | with open(file_path, "rb") as fh: 37 | tags = exifread.process_file( 38 | fh=fh, stop_tag=stop_tag, builtin_types=builtin_types 39 | ) 40 | assert len(tags) == tag_count 41 | 42 | 43 | @pytest.mark.parametrize("extract_thumbnail", (True, False)) 44 | def test_makernote_extract(extract_thumbnail): 45 | file_path = RESOURCES_ROOT / "jpg/Canon_DIGITAL_IXUS_400.jpg" 46 | with open(file_path, "rb") as fh: 47 | tags = exifread.process_file( 48 | fh=fh, extract_thumbnail=extract_thumbnail, details=True 49 | ) 50 | assert "MakerNote AESetting" in tags 51 | 52 | 53 | @pytest.mark.parametrize("extract_thumbnail", (True, False)) 54 | def test_no_makernote_extract(extract_thumbnail): 55 | file_path = RESOURCES_ROOT / "jpg/Canon_DIGITAL_IXUS_400.jpg" 56 | with open(file_path, "rb") as fh: 57 | tags = exifread.process_file( 58 | fh=fh, extract_thumbnail=extract_thumbnail, details=False 59 | ) 60 | assert "MakerNote AESetting" not in tags 61 | 62 | 63 | @pytest.mark.parametrize("details", (True, False)) 64 | def test_thumbnail_extract(details): 65 | file_path = RESOURCES_ROOT / "jpg/Canon_40D.jpg" 66 | with open(file_path, "rb") as fh: 67 | tags = exifread.process_file(fh=fh, extract_thumbnail=True, details=details) 68 | assert len(tags["JPEGThumbnail"]) == 1378 69 | 70 | 71 | @pytest.mark.parametrize("details", (True, False)) 72 | def test_no_thumbnail_extract(details): 73 | file_path = RESOURCES_ROOT / "jpg/Canon_40D.jpg" 74 | with open(file_path, "rb") as fh: 75 | tags = exifread.process_file(fh=fh, extract_thumbnail=False, details=details) 76 | assert "JPEGThumbnail" not in tags 77 | 78 | 79 | @pytest.mark.parametrize("strict", (True, False)) 80 | def test_no_exif(strict): 81 | file_path = RESOURCES_ROOT / "jpg/xmp/no_exif.jpg" 82 | with open(file_path, "rb") as fh: 83 | tags = exifread.process_file(fh=fh, details=True, strict=strict) 84 | assert not tags 85 | 86 | 87 | @pytest.mark.parametrize("strict", (True, False)) 88 | def test_invalid_exif(strict): 89 | file_path = RESOURCES_ROOT / "jpg/invalid/image00971.jpg" 90 | with open(file_path, "rb") as fh: 91 | tags = exifread.process_file(fh=fh, details=True, strict=strict) 92 | assert not tags 93 | 94 | 95 | @pytest.mark.parametrize( 96 | "file_path, message", 97 | ( 98 | ("jpg/tests/30-type_error.jpg", "corrupted IFD: EXIF"), 99 | ("jpg/tests/35-empty.jpg", "corrupted field RecordingMode"), 100 | ("jpg/tests/45-gps_ifd.jpg", "No values found for GPS SubIFD"), 101 | ), 102 | ) 103 | def test_warning_messages(caplog, file_path, message): 104 | """ 105 | We already capture this in the dump file. 106 | Need to make sure it's the logger capturing this rather than a print() statement or equivalent. 107 | """ 108 | caplog.set_level(logging.WARNING) 109 | with open(RESOURCES_ROOT / file_path, "rb") as fh: 110 | exifread.process_file(fh=fh, details=True) 111 | assert message in caplog.text 112 | 113 | 114 | def test_stop_tag_with_thumbnail_extract(): 115 | """ 116 | Stop at `Orientation` tag and extract thumbnail. 117 | Fails since the `Orientation` tag comes before the `JPEGInterchangeFormatLength` tag. 118 | Should not raise an Exception. 119 | """ 120 | file_path = RESOURCES_ROOT / "jpg/tests/Xiaomi_Mi_9T_KeyError.jpg" 121 | with open(file_path, "rb") as fh: 122 | tags = exifread.process_file(fh=fh, details=False, stop_tag="Orientation") 123 | assert tags 124 | 125 | 126 | @pytest.mark.parametrize("details", (True, False)) 127 | @pytest.mark.parametrize("truncate_tags", (True, False)) 128 | @pytest.mark.parametrize("stop_tag", ("WhiteBalance", DEFAULT_STOP_TAG)) 129 | def test_builtin_types(stop_tag, details, truncate_tags): 130 | """ 131 | When ``builtin_types=True``, always convert to Python types. 132 | Test with various other options to make sure they don't interfere. 133 | The "WhiteBalance" tag is after al the tags tested so must not have an impact. 134 | """ 135 | file_path = RESOURCES_ROOT / "jpg/Canon_DIGITAL_IXUS_400.jpg" 136 | with open(file_path, "rb") as fh: 137 | tags = exifread.process_file( 138 | fh=fh, 139 | builtin_types=True, 140 | stop_tag=stop_tag, 141 | details=details, 142 | truncate_tags=truncate_tags, 143 | ) 144 | # Short mapped to string value 145 | assert tags["EXIF ColorSpace"] == "sRGB" 146 | # Short 147 | assert isinstance(tags["EXIF ExifImageLength"], int) 148 | assert tags["EXIF ExifImageLength"] == 75 149 | # Ratio 150 | assert isinstance(tags["EXIF ExposureTime"], float) 151 | assert tags["EXIF ExposureTime"] == 0.005 152 | # ASCII 153 | assert tags["Image Make"] == "Canon" 154 | # Unknown / Undefined 155 | assert tags["EXIF FlashPixVersion"] == "0100" 156 | 157 | 158 | def test_xmp_no_tag(): 159 | """Read XMP data not in an Exif tag.""" 160 | 161 | file_path = RESOURCES_ROOT / "tiff/Arbitro.tiff" 162 | with open(file_path, "rb") as fh: 163 | tags = exifread.process_file( 164 | fh=fh, 165 | builtin_types=True, 166 | ) 167 | assert len(tags["Image ApplicationNotes"]) == 323 168 | --------------------------------------------------------------------------------