pax_global_header00006660000000000000000000000064142357744410014525gustar00rootroot0000000000000052 comment=3575cc6d0df9e356edc1cd3d49516d066d1ad465 exif-py-3.0.0/000077500000000000000000000000001423577444100131065ustar00rootroot00000000000000exif-py-3.0.0/.editorconfig000066400000000000000000000003421423577444100155620ustar00rootroot00000000000000# top-most EditorConfig file root = true [*] end_of_line = lf insert_final_newline = true # 4 space indentation [*.py] indent_style = space indent_size = 4 # 2 space indentation [*.yml] indent_style = space indent_size = 2 exif-py-3.0.0/.github/000077500000000000000000000000001423577444100144465ustar00rootroot00000000000000exif-py-3.0.0/.github/workflows/000077500000000000000000000000001423577444100165035ustar00rootroot00000000000000exif-py-3.0.0/.github/workflows/linting.yml000066400000000000000000000015641423577444100207000ustar00rootroot00000000000000# # Run static code analysis. # name: Static Analysis on: - push jobs: static-check: name: Run Static Analysis runs-on: ubuntu-latest strategy: matrix: python-version: ["3.8"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Cache dependencies uses: actions/cache@v2 with: path: ~/.cache/pip key: ${{ runner.os }}-dev-${{ hashFiles('setup.py') }} restore-keys: | ${{ runner.os }}-dev- - name: Install dependencies run: | pip install virtualenv make venv reqs-install - name: Analysing the code with mypy run: | make mypy - name: Analysing the code with pylint run: | make lint exif-py-3.0.0/.github/workflows/test.yml000066400000000000000000000023771423577444100202160ustar00rootroot00000000000000# # Run unit tests. # name: Test on: - pull_request jobs: pytest: name: Run Tests runs-on: ubuntu-latest timeout-minutes: 30 strategy: matrix: python-version: - "3.5" - "3.6" - "3.7" - "3.8" - "3.9" - "3.10" steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Cache dependencies uses: actions/cache@v2 with: path: ~/.cache/pip key: ${{ runner.os }}-test-${{ hashFiles('setup.py') }} restore-keys: | ${{ runner.os }}-test- - name: Download Samples run: | make samples-download - name: Install run: | pip install -e . - name: Run in debug and color mode run: | find exif-samples-master -name *.tiff -o -name *.jpg | xargs EXIF.py -dc - name: Compare image processing output run: | find exif-samples-master -name *.tiff -o -name *.jpg | sort -f | xargs EXIF.py > exif-samples-master/dump_test diff -Z --side-by-side --suppress-common-lines exif-samples-master/dump exif-samples-master/dump_test exif-py-3.0.0/.pylintrc000066400000000000000000000363151423577444100147630ustar00rootroot00000000000000[MASTER] # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. extension-pkg-whitelist= # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. ignore-patterns= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. jobs=0 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or # complex, nested conditions. limit-inference-results=100 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= # Pickle collected data for later comparisons. persistent=yes # Specify a configuration file. #rcfile= # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. confidence= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once). You can also use "--disable=all" to # disable everything first and then re-enable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". disable=missing-docstring, duplicate-code, # https://github.com/PyCQA/pylint/issues/214 # we should try to get rid of these at some point! fixme, consider-using-f-string, too-many-arguments, too-many-branches, # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable=c-extension-no-member [REPORTS] # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details. #msg-template= # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio). You can also give a reporter class, e.g. # mypackage.mymodule.MyReporterClass. output-format=text # Tells whether to display a full report or only the messages. reports=no # Activate the evaluation score. score=yes [REFACTORING] # Maximum number of nested blocks for function / method body max-nested-blocks=5 # Complete name of functions that never returns. When checking for # inconsistent-return-statements if a never returning function is called then # it will be considered as an explicit return statement and no message will be # printed. never-returning-functions=sys.exit [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME, XXX, TODO [SPELLING] # Limits count of emitted suggestions for spelling mistakes. max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package.. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [LOGGING] # Format style used to check logging format string. `old` means using % # formatting, while `new` is for `{}` formatting. logging-format-style=old # Logging modules to check that the string format arguments are in logging # function parameter format. logging-modules=logging [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid defining new builtins when possible. additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_, _cb # A regular expression matching the name of dummy variables (i.e. expected to # not be used). dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore. ignored-argument-names=_.*|^ignored_|^unused_ # Tells whether we should check for unused import in __init__ files. init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io [BASIC] # Naming style matching correct argument names. argument-naming-style=snake_case # Regular expression matching correct argument names. Overrides argument- # naming-style. #argument-rgx= # Naming style matching correct attribute names. attr-naming-style=snake_case # Regular expression matching correct attribute names. Overrides attr-naming- # style. #attr-rgx= # Bad variable names which should always be refused, separated by a comma. bad-names=foo, bar, baz, toto, tutu, tata # Naming style matching correct class attribute names. class-attribute-naming-style=any # Regular expression matching correct class attribute names. Overrides class- # attribute-naming-style. #class-attribute-rgx= # Naming style matching correct class names. class-naming-style=PascalCase # Regular expression matching correct class names. Overrides class-naming- # style. #class-rgx= # Naming style matching correct constant names. const-naming-style=UPPER_CASE # Regular expression matching correct constant names. Overrides const-naming- # style. #const-rgx= # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 # Naming style matching correct function names. function-naming-style=snake_case # Regular expression matching correct function names. Overrides function- # naming-style. #function-rgx= # Good variable names which should always be accepted, separated by a comma. good-names=i, s, k, ex, Run, _, fh, # Include a hint for the correct naming format with invalid-name. include-naming-hint=no # Naming style matching correct inline iteration names. inlinevar-naming-style=any # Regular expression matching correct inline iteration names. Overrides # inlinevar-naming-style. #inlinevar-rgx= # Naming style matching correct method names. method-naming-style=snake_case # Regular expression matching correct method names. Overrides method-naming- # style. #method-rgx= # Naming style matching correct module names. module-naming-style=snake_case # Regular expression matching correct module names. Overrides module-naming- # style. #module-rgx= # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. # These decorators are taken in consideration only for invalid-name. property-classes=abc.abstractproperty # Naming style matching correct variable names. variable-naming-style=snake_case # Regular expression matching correct variable names. Overrides variable- # naming-style. #variable-rgx= [TYPECHECK] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members= # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # Tells whether to warn about missing members when the owner of the attribute # is inferred to be None. ignore-none=no # This flag controls whether pylint should warn about no-member and similar # checks whenever an opaque object is returned when inferring. The inference # can return multiple potential results while evaluating a Python object, but # some branches might not be evaluated, which results in partial inference. In # that case, it might be useful to still emit no-member and other checks for # the rest of the inferred objects. ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. ignored-classes=optparse.Values,thread._local,_thread._local # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. ignored-modules= # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. missing-member-hint=yes # The minimum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 # The total number of similar names that should be taken in consideration when # showing a hint for a missing member. missing-member-max-choices=1 [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format=LF # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. max-line-length=120 # Maximum number of lines in a module. max-module-lines=1000 # List of optional constructs for which whitespace checking is disabled. `dict- # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. # `trailing-comma` allows a space between comma and closing bracket: (a, ). # `empty-line` allows space-only lines. no-space-check=trailing-comma, dict-separator # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no [SIMILARITIES] # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no # Minimum lines number of a similarity. min-similarity-lines=4 [STRING] # This flag controls whether the implicit-str-concat-in-sequence should # generate a warning on implicit string concatenation in sequences defined over # several lines. check-str-concat-over-line-jumps=no [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict, _fields, _replace, _source, _make # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=cls [IMPORTS] # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=no # Deprecated modules which should not be used, separated by a comma. deprecated-modules=optparse,tkinter.tix # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled). ext-import-graph= # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled). import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled). int-import-graph= # Force import order to recognize a module as part of the standard # compatibility libraries. known-standard-library= # Force import order to recognize a module as part of a third party library. known-third-party=enchant [DESIGN] # Maximum number of arguments for function / method. max-args=5 # Maximum number of attributes for a class (see R0902). max-attributes=10 # Maximum number of boolean expressions in an if statement. max-bool-expr=5 # Maximum number of branch for function / method body. max-branches=10 # Maximum number of locals for function / method body. max-locals=20 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # Maximum number of return / yield for function / method body. max-returns=10 # Maximum number of statements in function / method body. max-statements=50 # Minimum number of public methods for a class (see R0903). min-public-methods=2 [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". overgeneral-exceptions=BaseException, Exception exif-py-3.0.0/CONTRIBUTING.rst000066400000000000000000000015251423577444100155520ustar00rootroot00000000000000************ Contributing ************ All contributions are welcome! Bug reports, feature requests, bug fixes, documentation updates, sample images, etc ... Nothing is too small or too big ;-) Please be aware that this is a purely spare time project, so don't be offended if it takes some time to answer. Code Contributions ****************** Please start from the ``develop`` branch for any code or documentation contributions. You may even find the work has already been done... Normally the ``master`` branch is only for stable, released code. Sample Images ************* Sample images are very important, allowing for validating new features and limiting regressions. On every build, the library is run on all images. The samples are kept in a separate repository for space and bandwidth concerns: https://github.com/ianare/exif-samples exif-py-3.0.0/ChangeLog.rst000066400000000000000000000215471423577444100155000ustar00rootroot00000000000000********** Change Log ********** 3.0.0 — 2022-05-08 * **BREAKING CHANGE:** Add type hints, which removes Python2 compatibility * Update make_string util to clean up bad values (#128) by Étienne Pelletier * Fix Olympus SpecialMode Unknown Values (#143) by Paul Barton * Remove coding system from UserComment sequence only if it is valid (#147) by Grzegorz Ruciński * Fixes to orientation by Mark * Add some EXIF tags * Add support for PNG files (#159) by Marco * Fix for HEIC Unknown Parsers (#153) by Paul Barton * Handle images that has corrupted headers/tags (#152) by Mahmoud Harmouch 2.3.2 — 2020-10-29 * Fixes for HEIC files from Note10+ (#127) by Drew Perttula * Add missing EXIF OffsetTime tags (#126) by Étienne Pelletier 2.3.1 — 2020-08-07 * Fix bug introduced with v2.3.0 in HEIC processing. 2.3.0 — 2020-08-03 * Add notice on Python2 EOL * Modernize code and improve testing, split up some huge functions * Added support for webp file format (#116) by Grzegorz Ruciński * Add linting * Added missing IFD data type; correct spelling mistake (#119) by Piero Toffanin * Add syntax highlight for README (#117) by John Lin * Add Python 3.8 to CI (#113) by 2*yo * make HEIC exif extractor much more compatible (#109) by Tony Guo * Add black level tag (#108) * Use list instead of tuple for classifiers (#107) by Florian Preinstorfer 2.2.1 — 2020-07-31 * Very minor corrections. 2.2.0 — 2019-07-24 * Add support for Python 3.5, 3.6, 3.7 * Drop official support for Python 2.6, 3.2, 3.3 * Fix for string count equals 0 (issue #67) * Rebasing of struct pull requests: closes #54, closes #60 by Christopher Chavez * Refactor to use Python's struct module for packing/unpacking by Dave Jones (waveform80) * Support floating point fields" by Reed Nightingale (reedbn) * Raw images support by changing Tiff detection by xaumex * Fix GPS information erroneously None (#96) by Christopher Chavez * Initial HEIC support (Sam Rushing) 2.1.2 — 2015-09-14 * Fix 90 CW (6) and Rotated 90 CCW (8) which were swapped with each other by Mark Hahnenberg * Catch memory and overflow errors on file seek, print a warning * Put manufacturers' makernote definitions in separate files 2.1.1 — 2015-05-16 * Add a CONTRIBUTING file for Github. * Add some FujiFilm tags. * Revert Canon Makernote processing modifications 2.1.0 — 2015-05-15 * Bypass empty/unreadable Olympus MakerNote info (issue #42) * Support Apple Makernote and Apple HDR details by Jesus Cea * Correcty process the Makernote of some Canon models by Jesus Cea * Support HDR in Canon cameras by Jesus Cea 2.0.2 — 2015-03-29 * Fixed bug when importing as a module (issue #31) 2.0.1 — 2014-02-09 * Represent the IFD as a string to fix formatting errors (issue #45) * Fix unicode errors in python2 (issue #46) * Fix for tag name backwards compatibility with 1.X series 2.0.0 — 2014-11-27 * Drop support for Python 2.5 * Add support for Python 3.2, 3.3 and 3.4 by velis74 * Add Travis testing * Cleanup some tag definitions * Fix bug #30 (TypeError on invalid IFD) * Fix bug #33 (TypeError on invalid output characters) * Add basic coloring for debug mode * Add finding XMP tags (experimental, debug only) * Add some missing Exif tags * Use stdout for log output * Experimental support for dumping XMP data 1.4.2 — 2013-11-28 * A few new Canon tags * Python3 fixes by velis74 and leprechaun * Fix for TypeError (issue #28) * Pylint & PEP8 fixes 1.4.1 — 2013-10-19 * Better version handling * Better PyPI packaging 1.4.0 — 2013-09-28 * Many new tags big thanks to Rodolfo Puig, Paul Barton, Joe Beda * Do not extract thumbnail in quick mode (issue #19) * Put tag definitions in separate module * Add more timing info & version info 1.3.3 — 2013-08-03 * Add timing info in debug mode and nicer message format * Fix for faster processing 1.3.2 — 2013-07-31 * Improve PyPI package * fix for DeprecationWarning: classic int division * Improvements to debug output * Add some Nikon makernote tags 1.3.1 — 2013-07-29 * More PEP8 & PEP257 improvements * Better logging 1.3.0 — 2013-07-27 * Set default values in case not set (ortsed) * PEP8 & PEP257 improvements * Better score in pylint * Ideas and some code from Samuele Santi's and Peter Reimer's forks * Replace print with logging * Package for PyPI 1.2.0 — 2013-02-08 * Port to Python 3 by DarkRedman * Fix endless loop on broken images by Michael Bemmerl * Rewrite of README.md * Fixed incoherent copyright notices 1.1.0 — 2012-11-30 - all by Gregory Dudek * Overflow error fixes added (related to 2**31 size) * GPS tags added. 1.0.10 — 2012-09-26 * Add GPS tags * Add better endian debug info 2012-06-13 * Support malformed last IFD by fhats * Light source, Flash and Metering mode dictionaries by gryfik 2008-07-31 * Wikipedia Commons hunt for suitable test case images, * testing new code additions. 2008-07-09 - all by Stephen H. Olson * Fix a problem with reading MakerNotes out of NEF files. * Add some more Nikon MakerNote tags. 2008-07-08 - all by Stephen H. Olson * An error check for large tags totally borked MakerNotes. With Nikon anyway, valid MakerNotes can be pretty big. * Add error check for a crash caused by nikon_ev_bias being called with the wrong args. * Drop any garbage after a null character in string (patch from Andrew McNabb ). 2008-02-12 * Fix crash on invalid MakerNote * Fix crash on huge Makernote (temp fix) * Add printIM tag 0xC4A5, needs decoding info * Add 0x9C9B-F range of tags * Add a bunch of tag definitions from: http://owl.phy.queensu.ca/~phil/exiftool/TagNames/EXIF.html * Add 'strict' variable and command line option 2008-01-18 - all by Gunter Ohrner * Add ``GPSDate`` tag 2007-12-12 * Fix quick option on certain image types * Add note on tag naming in documentation 2007-11-30 * Changed -s option to -t * Put changelog into separate file 2007-10-28 * Merged changes from ReimarBauer * Added command line option for debug, stop processing on tag. 2007-09-27 * Add some Olympus Makernote tags. 2007-09-26 - all by Stephen H. Olson * Don't error out on invalid Olympus 'SpecialMode'. * Add a few more Olympus/Minolta tags. 2007-09-22 - all by Stephen H. Olson * Don't error on invalid string * Improved Nikon MakerNote support 2007-05-03 - all by Martin Stone * Fix for inverted detailed flag and Photoshop header 2007-03-24 * Can now ignore MakerNotes Tags for faster processing. 2007-01-18 * Fixed a couple errors and assuming maintenance of the library. 2006-08-04 all by Reimar Bauer * Added an optional parameter name to process_file and dump_IFD. Using this parameter the loop is breaked after that tag_name is processed. * some PEP8 changes Original Notices **************** Contains code from "exifdump.py" originally written by Thierry Bousch and released into the public domain. Updated and turned into general-purpose library by Gene Cash Patch Contributors: * Simon J. Gerraty s2n fix & orientation decode * John T. Riedl Added support for newer Nikon type 3 Makernote format for D70 and some other Nikon cameras. * Joerg Schaefer Fixed subtle bug when faking an EXIF header, which affected maker notes using relative offsets, and a fix for Nikon D100. 2004-02-15 CEC * Finally fixed bit shift warning by converting Y to 0L. 2003-11-30 CEC * Fixed problem with canon_decode_tag() not creating an IFD_Tag() object. 2002-01-26 CEC * Added ability to extract TIFF thumbnails. * Added Nikon, Fujifilm, Casio MakerNotes. 2002-01-25 CEC * Discovered JPEG thumbnail in Olympus TIFF MakerNote. 2002-01-23 CEC * Trimmed nulls from end of string values. 2002-01-20 CEC Added MakerNote processing logic. * Added Olympus MakerNote. * Converted data structure to single-level dictionary, avoiding tag name collisions by prefixing with IFD name. This makes it much easier to use. 2002-01-19 CEC Added ability to read TIFFs and JFIF-format JPEGs. * Added ability to extract JPEG formatted thumbnail. * Added ability to read GPS IFD (not tested). * Converted IFD data structure to dictionaries indexed by tag name. * Factored into library returning dictionary of IFDs plus thumbnail, if any. 2002-01-17 CEC Discovered code on web. * Commented everything. * Made small code improvements. * Reformatted for readability. 1999-08-21 TB * Last update by Thierry Bousch to his code. exif-py-3.0.0/EXIF.py000077500000000000000000000070511423577444100142210ustar00rootroot00000000000000#!/usr/bin/env python3 # # # Library to extract Exif information from digital camera image files. # https://github.com/ianare/exif-py # # # Copyright (c) 2002-2007 Gene Cash # Copyright (c) 2007-2022 Ianaré Sévi and contributors # # See LICENSE.txt file for licensing information # See ChangeLog.rst file for all contributors and changes # """ Runs Exif tag extraction in command line. """ import sys import argparse import timeit from exifread.tags import FIELD_TYPES from exifread import process_file, exif_log, __version__ logger = exif_log.get_logger() def get_args(): parser = argparse.ArgumentParser( prog='EXIF.py', description='Extract EXIF information from digital image files.' ) parser.add_argument( 'files', metavar='FILE', type=str, nargs='+', help='files to process', ) parser.add_argument( '-v', '--version', action='version', version='EXIF.py Version %s on Python%s' % (__version__, sys.version_info[0]), help='Display version information and exit' ) parser.add_argument( '-q', '--quick', action='store_false', dest='detailed', help='Do not process MakerNotes', ) parser.add_argument( '-t', '--tag', type=str, dest='stop_tag', help='Stop processing when this tag is retrieved.', ) parser.add_argument( '-s', '--strict', action='store_true', dest='strict', help='Run in strict mode (stop on errors).', ) parser.add_argument( '-d', '--debug', action='store_true', dest='debug', help='Run in debug mode (display extra info).', ) parser.add_argument( '-c', '--color', action='store_true', dest='color', help='Output in color (only works with debug on POSIX).', ) args = parser.parse_args() return args def main(args) -> None: """Extract tags based on options (args).""" exif_log.setup_logger(args.debug, args.color) # output info for each file for filename in args.files: # avoid errors when printing to console escaped_fn = escaped_fn = filename.encode( sys.getfilesystemencoding(), 'surrogateescape' ).decode() file_start = timeit.default_timer() try: img_file = open(escaped_fn, 'rb') except IOError: logger.error("'%s' is unreadable", escaped_fn) continue logger.info('Opening: %s', escaped_fn) tag_start = timeit.default_timer() # get the tags data = process_file( img_file, stop_tag=args.stop_tag, details=args.detailed, strict=args.strict, debug=args.debug ) tag_stop = timeit.default_timer() if not data: logger.warning('No EXIF information found') print() continue if 'JPEGThumbnail' in data: logger.info('File has JPEG thumbnail') del data['JPEGThumbnail'] if 'TIFFThumbnail' in data: logger.info('File has TIFF thumbnail') del data['TIFFThumbnail'] tag_keys = list(data.keys()) tag_keys.sort() for i in tag_keys: try: logger.info('%s (%s): %s', i, FIELD_TYPES[data[i].field_type][2], data[i].printable) except: logger.error("%s : %s", i, str(data[i])) file_stop = timeit.default_timer() logger.debug("Tags processed in %s seconds", tag_stop - tag_start) logger.debug("File processed in %s seconds", file_stop - file_start) print() if __name__ == '__main__': main(get_args()) exif-py-3.0.0/LICENSE.txt000066400000000000000000000030001423577444100147220ustar00rootroot00000000000000 Copyright (c) 2002-2007 Gene Cash Copyright (c) 2007-2021 Ianaré Sévi and contributors Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the authors nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. exif-py-3.0.0/MANIFEST.in000066400000000000000000000000251423577444100146410ustar00rootroot00000000000000include *.txt *.rst exif-py-3.0.0/Makefile000066400000000000000000000027421423577444100145530ustar00rootroot00000000000000 ifneq (,$(wildcard /.dockerenv)) PYTHON_BIN := /usr/local/bin/python3 PIP_BIN := /usr/local/bin/pip3 PYLINT_BIN := ~/.local/bin/pylint MYPY_BIN := ~/.local/bin/mypy TWINE_BIN := ~/.local/bin/twine PIP_INSTALL := $(PIP_BIN) install --progress-bar=off --user else VENV_DIR := ./.venv PYTHON_BIN := $(VENV_DIR)/bin/python3 PIP_BIN := $(VENV_DIR)/bin/pip3 PYLINT_BIN := $(VENV_DIR)/bin/pylint MYPY_BIN := $(VENV_DIR)/bin/mypy TWINE_BIN := $(VENV_DIR)/bin/twine PIP_INSTALL := $(PIP_BIN) install --progress-bar=off endif .PHONY: help all: help venv: ## Set up the virtual environment virtualenv -p python3 $(VENV_DIR) lint: ## Run linting (pylint) $(PYLINT_BIN) -f colorized ./exifread mypy: ## Run mypy $(MYPY_BIN) --show-error-context ./exifread ./EXIF.py #test: ## Run all tests # $(PYTHON_BIN) -m unittest discover -v -s ./tests analyze: lint mypy ## Run all static analysis tools reqs-install: ## Install with all requirements $(PIP_INSTALL) .[dev] samples-download: ## Install sample files used for testing. wget https://github.com/ianare/exif-samples/archive/master.tar.gz tar -xzf master.tar.gz build: ## build distribution rm -fr ./dist $(PYTHON_BIN) setup.py sdist publish: build ## Publish to PyPI $(TWINE_BIN) upload --repository testpypi dist/* help: Makefile @echo @echo "Choose a command to run:" @echo @grep --no-filename -E '^[a-zA-Z_%-]+:.*?## .*$$' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-30s\033[0m %s\n", $$1, $$2}' @echo exif-py-3.0.0/README.rst000066400000000000000000000125251423577444100146020ustar00rootroot00000000000000******* EXIF.py ******* Easy to use Python module to extract Exif metadata from digital image files. Supported formats: TIFF, JPEG, PNG, Webp, HEIC Compatibility ************* EXIF.py is tested and officially supported on Python 3.5 to 3.10 Starting with version ``3.0.0``, Python2 compatibility is dropped *completely* (syntax errors due to type hinting). https://pythonclock.org/ Installation ************ Stable Version ============== The recommended process is to install the `PyPI package `_, as it allows easily staying up to date:: $ pip install exifread See the `pip documentation `_ for more info. EXIF.py is mature software and strives for stability. Development Version =================== After cloning the repo, use the provided Makefile:: make venv reqs-install Which will install a virtual environment and install development dependencies. Usage ***** Command line ============ Some examples:: EXIF.py image1.jpg EXIF.py -dc image1.jpg image2.tiff find ~/Pictures -name "*.jpg" -o -name "*.tiff" | xargs EXIF.py Show command line options:: EXIF.py -h Python Script ============= .. code-block:: python import exifread # Open image file for reading (must be in binary mode) f = open(path_name, 'rb') # Return Exif tags tags = exifread.process_file(f) *Note:* To use this library in your project as a Git submodule, you should:: from import exifread Returned tags will be a dictionary mapping names of Exif tags to their values in the file named by path_name. You can process the tags as you wish. In particular, you can iterate through all the tags with: .. code-block:: python for tag in tags.keys(): if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename', 'EXIF MakerNote'): print "Key: %s, value %s" % (tag, tags[tag]) An ``if`` statement is used to avoid printing out a few of the tags that tend to be long or boring. The tags dictionary will include keys for all of the usual Exif tags, and will also include keys for Makernotes used by some cameras, for which we have a good specification. Note that the dictionary keys are the IFD name followed by the tag name. For example:: 'EXIF DateTimeOriginal', 'Image Orientation', 'MakerNote FocusMode' Tag Descriptions **************** Tags are divided into these main categories: - ``Image``: information related to the main image (IFD0 of the Exif data). - ``Thumbnail``: information related to the thumbnail image, if present (IFD1 of the Exif data). - ``EXIF``: Exif information (sub-IFD). - ``GPS``: GPS information (sub-IFD). - ``Interoperability``: Interoperability information (sub-IFD). - ``MakerNote``: Manufacturer specific information. There are no official published references for these tags. Processing Options ****************** These options can be used both in command line mode and within a script. Faster Processing ================= Don't process makernote tags, don't extract the thumbnail image (if any). Pass the ``-q`` or ``--quick`` command line arguments, or as: .. code-block:: python tags = exifread.process_file(f, details=False) Stop at a Given Tag =================== To stop processing the file after a specified tag is retrieved. Pass the ``-t TAG`` or ``--stop-tag TAG`` argument, or as: .. code-block:: python tags = exifread.process_file(f, stop_tag='TAG') where ``TAG`` is a valid tag name, ex ``'DateTimeOriginal'``. *The two above options are useful to speed up processing of large numbers of files.* Strict Processing ================= Return an error on invalid tags instead of silently ignoring. Pass the ``-s`` or ``--strict`` argument, or as: .. code-block:: python tags = exifread.process_file(f, strict=True) Usage Example ============= This example shows how to use the library to correct the orientation of an image (using Pillow for the transformation) before e.g. displaying it. .. code-block:: python import exifread from PIL import Image import logging def _read_img_and_correct_exif_orientation(path): im = Image.open(path) tags = {} with open(path, 'rb') as f: tags = exifread.process_file(f, details=False) if "Image Orientation" in tags.keys(): orientation = tags["Image Orientation"] logging.basicConfig(level=logging.DEBUG) logging.debug("Orientation: %s (%s)", orientation, orientation.values) val = orientation.values if 2 in val: val += [4, 3] if 5 in val: val += [4, 6] if 7 in val: val += [4, 8] if 3 in val: logging.debug("Rotating by 180 degrees.") im = im.transpose(Image.ROTATE_180) if 4 in val: logging.debug("Mirroring horizontally.") im = im.transpose(Image.FLIP_TOP_BOTTOM) if 6 in val: logging.debug("Rotating by 270 degrees.") im = im.transpose(Image.ROTATE_270) if 8 in val: logging.debug("Rotating by 90 degrees.") im = im.transpose(Image.ROTATE_90) return im Credit ****** A huge thanks to all the contributors over the years! Originally written by Gene Cash & Thierry Bousch. exif-py-3.0.0/exifread/000077500000000000000000000000001423577444100146755ustar00rootroot00000000000000exif-py-3.0.0/exifread/__init__.py000066400000000000000000000143201423577444100170060ustar00rootroot00000000000000""" Read Exif metadata from tiff and jpeg files. """ import struct from typing import BinaryIO from exifread.exif_log import get_logger from exifread.classes import ExifHeader from exifread.tags import DEFAULT_STOP_TAG from exifread.utils import ord_, make_string from exifread.heic import HEICExifFinder from exifread.jpeg import find_jpeg_exif from exifread.exceptions import InvalidExif, ExifNotFound __version__ = '3.0.0' logger = get_logger() def _find_tiff_exif(fh: BinaryIO) -> tuple: logger.debug("TIFF format recognized in data[0:2]") fh.seek(0) endian = fh.read(1) fh.read(1) offset = 0 return offset, endian def _find_webp_exif(fh: BinaryIO) -> tuple: logger.debug("WebP format recognized in data[0:4], data[8:12]") # file specification: https://developers.google.com/speed/webp/docs/riff_container data = fh.read(5) if data[0:4] == b'VP8X' and data[4] & 8: # https://developers.google.com/speed/webp/docs/riff_container#extended_file_format fh.seek(13, 1) while True: data = fh.read(8) # Chunk FourCC (32 bits) and Chunk Size (32 bits) if len(data) != 8: raise InvalidExif("Invalid webp file chunk header.") if data[0:4] == b'EXIF': offset = fh.tell() endian = fh.read(1) return offset, endian size = struct.unpack(' tuple: logger.debug("PNG format recognized in data[0:8]=%s", data[:8].hex()) fh.seek(8) while True: data = fh.read(8) chunk = data[4:8] logger.debug("PNG found chunk %s", chunk.decode("ascii")) if chunk in (b'', b'IEND'): break if chunk == b'eXIf': offset = fh.tell() return offset, fh.read(1) chunk_size = int.from_bytes(data[:4], "big") fh.seek(fh.tell() + chunk_size + 4) raise ExifNotFound("PNG file does not have exif data.") def _get_xmp(fh: BinaryIO) -> bytes: xmp_bytes = b'' logger.debug('XMP not in Exif, searching file for XMP info...') xml_started = False xml_finished = False for line in fh: open_tag = line.find(b'') if open_tag != -1: xml_started = True line = line[open_tag:] logger.debug('XMP found opening tag at line position %s', open_tag) if close_tag != -1: logger.debug('XMP found closing tag at line position %s', close_tag) line_offset = 0 if open_tag != -1: line_offset = open_tag line = line[:(close_tag - line_offset) + 12] xml_finished = True if xml_started: xmp_bytes += line if xml_finished: break logger.debug('XMP Finished searching for info') return xmp_bytes def _determine_type(fh: BinaryIO) -> tuple: # by default do not fake an EXIF beginning fake_exif = 0 data = fh.read(12) if data[0:2] in [b'II', b'MM']: # it's a TIFF file offset, endian = _find_tiff_exif(fh) elif data[4:12] == b'ftypheic': fh.seek(0) heic = HEICExifFinder(fh) offset, endian = heic.find_exif() elif data[0:4] == b'RIFF' and data[8:12] == b'WEBP': offset, endian = _find_webp_exif(fh) elif data[0:2] == b'\xFF\xD8': # it's a JPEG file offset, endian, fake_exif = find_jpeg_exif(fh, data, fake_exif) elif data[0:8] == b'\x89PNG\r\n\x1a\n': offset, endian = _find_png_exif(fh, data) else: # file format not recognized raise ExifNotFound("File format not recognized.") return offset, endian, fake_exif def process_file(fh: BinaryIO, stop_tag=DEFAULT_STOP_TAG, details=True, strict=False, debug=False, truncate_tags=True, auto_seek=True): """ Process an image file (expects an open file object). This is the function that has to deal with all the arbitrary nasty bits of the EXIF standard. """ if auto_seek: fh.seek(0) try: offset, endian, fake_exif = _determine_type(fh) except ExifNotFound as err: logger.warning(err) return {} except InvalidExif as err: logger.debug(err) return {} endian = chr(ord_(endian[0])) # deal with the EXIF info we found logger.debug("Endian format is %s (%s)", endian, { 'I': 'Intel', 'M': 'Motorola', '\x01': 'Adobe Ducky', 'd': 'XMP/Adobe unknown' }[endian]) hdr = ExifHeader(fh, endian, offset, fake_exif, strict, debug, details, truncate_tags) ifd_list = hdr.list_ifd() thumb_ifd = 0 ctr = 0 for ifd in ifd_list: if ctr == 0: ifd_name = 'Image' elif ctr == 1: ifd_name = 'Thumbnail' thumb_ifd = ifd else: ifd_name = 'IFD %d' % ctr logger.debug('IFD %d (%s) at offset %s:', ctr, ifd_name, ifd) hdr.dump_ifd(ifd, ifd_name, stop_tag=stop_tag) ctr += 1 # EXIF IFD exif_off = hdr.tags.get('Image ExifOffset') if exif_off: logger.debug('Exif SubIFD at offset %s:', exif_off.values[0]) hdr.dump_ifd(exif_off.values[0], 'EXIF', stop_tag=stop_tag) # deal with MakerNote contained in EXIF IFD # (Some apps use MakerNote tags but do not use a format for which we # have a description, do not process these). if details and 'EXIF MakerNote' in hdr.tags and 'Image Make' in hdr.tags: hdr.decode_maker_note() # extract thumbnails if details and thumb_ifd: hdr.extract_tiff_thumbnail(thumb_ifd) hdr.extract_jpeg_thumbnail() # parse XMP tags (experimental) if debug and details: # Easy we already have them xmp_tag = hdr.tags.get('Image ApplicationNotes') if xmp_tag: logger.debug('XMP present in Exif') xmp_bytes = bytes(xmp_tag.values) # We need to look in the entire file for the XML else: xmp_bytes = _get_xmp(fh) if xmp_bytes: hdr.parse_xmp(xmp_bytes) return hdr.tags exif-py-3.0.0/exifread/classes.py000066400000000000000000000604701423577444100167130ustar00rootroot00000000000000import re import struct from typing import BinaryIO, Dict, Any from exifread.exif_log import get_logger from exifread.utils import Ratio from exifread.tags import EXIF_TAGS, DEFAULT_STOP_TAG, FIELD_TYPES, IGNORE_TAGS, makernote logger = get_logger() class IfdTag: """ Eases dealing with tags. """ def __init__(self, printable: str, tag: int, field_type: int, values, field_offset: int, field_length: int): # printable version of data self.printable = printable # tag ID number self.tag = tag # field type as index into FIELD_TYPES self.field_type = field_type # offset of start of field in bytes from beginning of IFD self.field_offset = field_offset # length of data field in bytes self.field_length = field_length # either string, bytes or list of data items # TODO: sort out this type mess! self.values = values def __str__(self) -> str: return self.printable def __repr__(self) -> str: try: tag = '(0x%04X) %s=%s @ %d' % ( self.tag, FIELD_TYPES[self.field_type][2], self.printable, self.field_offset ) except TypeError: tag = '(%s) %s=%s @ %s' % ( str(self.tag), FIELD_TYPES[self.field_type][2], self.printable, str(self.field_offset) ) return tag class ExifHeader: """ Handle an EXIF header. """ def __init__(self, file_handle: BinaryIO, endian, offset, fake_exif, strict: bool, debug=False, detailed=True, truncate_tags=True): self.file_handle = file_handle self.endian = endian self.offset = offset self.fake_exif = fake_exif self.strict = strict self.debug = debug self.detailed = detailed self.truncate_tags = truncate_tags # TODO: get rid of 'Any' type self.tags = {} # type: Dict[str, Any] def s2n(self, offset, length: int, signed=False) -> int: """ Convert slice to integer, based on sign and endian flags. Usually this offset is assumed to be relative to the beginning of the start of the EXIF information. For some cameras that use relative tags, this offset may be relative to some other starting point. """ # Little-endian if Intel, big-endian if Motorola fmt = '<' if self.endian == 'I' else '>' # Construct a format string from the requested length and signedness; # raise a ValueError if length is something silly like 3 try: fmt += { (1, False): 'B', (1, True): 'b', (2, False): 'H', (2, True): 'h', (4, False): 'I', (4, True): 'i', (8, False): 'L', (8, True): 'l', }[(length, signed)] except KeyError as err: raise ValueError('unexpected unpacking length: %d' % length) from err self.file_handle.seek(self.offset + offset) buf = self.file_handle.read(length) if buf: # https://github.com/ianare/exif-py/pull/158 # had to revert as this certain fields to be empty # please provide test images return struct.unpack(fmt, buf)[0] return 0 def n2b(self, offset, length) -> bytes: """Convert offset to bytes.""" s = b'' for _ in range(length): if self.endian == 'I': s += bytes([offset & 0xFF]) else: s = bytes([offset & 0xFF]) + s offset = offset >> 8 return s def _first_ifd(self) -> int: """Return first IFD.""" return self.s2n(4, 4) def _next_ifd(self, ifd) -> int: """Return the pointer to next IFD.""" entries = self.s2n(ifd, 2) next_ifd = self.s2n(ifd + 2 + 12 * entries, 4) if next_ifd == ifd: return 0 return next_ifd def list_ifd(self) -> list: """Return the list of IFDs in the header.""" i = self._first_ifd() ifds = [] set_ifds = set() while i: if i in set_ifds: logger.warning('IFD loop detected.') break set_ifds.add(i) ifds.append(i) i = self._next_ifd(i) return ifds def _process_field(self, tag_name, count, field_type, type_length, offset): values = [] signed = (field_type in [6, 8, 9, 10]) # XXX investigate # some entries get too big to handle could be malformed # file or problem with self.s2n if count < 1000: for _ in range(count): if field_type in (5, 10): # a ratio value = Ratio( self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed) ) elif field_type in (11, 12): # a float or double unpack_format = '' if self.endian == 'I': unpack_format += '<' else: unpack_format += '>' if field_type == 11: unpack_format += 'f' else: unpack_format += 'd' self.file_handle.seek(self.offset + offset) byte_str = self.file_handle.read(type_length) try: value = struct.unpack(unpack_format, byte_str) except struct.error: logger.warning('Possibly corrupted field %s', tag_name) # -1 means corrupted value = -1 else: value = self.s2n(offset, type_length, signed) values.append(value) offset = offset + type_length # The test above causes problems with tags that are # supposed to have long values! Fix up one important case. elif tag_name in ('MakerNote', makernote.canon.CAMERA_INFO_TAG_NAME): for _ in range(count): value = self.s2n(offset, type_length, signed) values.append(value) offset = offset + type_length return values def _process_field2(self, ifd_name, tag_name, count, offset): values = '' # special case: null-terminated ASCII string # XXX investigate # sometimes gets too big to fit in int value if count != 0: # and count < (2**31): # 2E31 is hardware dependent. --gd file_position = self.offset + offset try: self.file_handle.seek(file_position) values = self.file_handle.read(count) # Drop any garbage after a null. values = values.split(b'\x00', 1)[0] if isinstance(values, bytes): try: values = values.decode('utf-8') except UnicodeDecodeError: logger.warning('Possibly corrupted field %s in %s IFD', tag_name, ifd_name) except OverflowError: logger.warning('OverflowError at position: %s, length: %s', file_position, count) values = '' except MemoryError: logger.warning('MemoryError at position: %s, length: %s', file_position, count) values = '' return values def _process_tag(self, ifd, ifd_name: str, tag_entry, entry, tag: int, tag_name, relative, stop_tag) -> None: field_type = self.s2n(entry + 2, 2) # unknown field type if not 0 < field_type < len(FIELD_TYPES): if not self.strict: return raise ValueError('Unknown type %d in tag 0x%04X' % (field_type, tag)) type_length = FIELD_TYPES[field_type][0] count = self.s2n(entry + 4, 4) # Adjust for tag id/type/count (2+2+4 bytes) # Now we point at either the data or the 2nd level offset offset = entry + 8 # If the value fits in 4 bytes, it is inlined, else we # need to jump ahead again. if count * type_length > 4: # offset is not the value; it's a pointer to the value # if relative we set things up so s2n will seek to the right # place when it adds self.offset. Note that this 'relative' # is for the Nikon type 3 makernote. Other cameras may use # other relative offsets, which would have to be computed here # slightly differently. if relative: tmp_offset = self.s2n(offset, 4) offset = tmp_offset + ifd - 8 if self.fake_exif: offset += 18 else: offset = self.s2n(offset, 4) field_offset = offset values = None if field_type == 2: values = self._process_field2(ifd_name, tag_name, count, offset) else: values = self._process_field(tag_name, count, field_type, type_length, offset) # now 'values' is either a string or an array # TODO: use only one type if count == 1 and field_type != 2: printable = str(values[0]) elif count > 50 and len(values) > 20 and not isinstance(values, str): if self.truncate_tags: printable = str(values[0:20])[0:-1] + ', ... ]' else: printable = str(values[0:-1]) else: printable = str(values) # compute printable version of values if tag_entry: # optional 2nd tag element is present if len(tag_entry) != 1: if callable(tag_entry[1]): # call mapping function printable = tag_entry[1](values) elif isinstance(tag_entry[1], tuple): ifd_info = tag_entry[1] try: logger.debug('%s SubIFD at offset %d:', ifd_info[0], values[0]) self.dump_ifd(values[0], ifd_info[0], tag_dict=ifd_info[1], stop_tag=stop_tag) except IndexError: logger.warning('No values found for %s SubIFD', ifd_info[0]) else: printable = '' for val in values: # use lookup table for this tag printable += tag_entry[1].get(val, repr(val)) self.tags[ifd_name + ' ' + tag_name] = IfdTag( printable, tag, field_type, values, field_offset, count * type_length ) tag_value = repr(self.tags[ifd_name + ' ' + tag_name]) logger.debug(' %s: %s', tag_name, tag_value) def dump_ifd(self, ifd, ifd_name: str, tag_dict=None, relative=0, stop_tag=DEFAULT_STOP_TAG) -> None: """ Return a list of entries in the given IFD. """ # make sure we can process the entries if tag_dict is None: tag_dict = EXIF_TAGS try: entries = self.s2n(ifd, 2) except TypeError: logger.warning('Possibly corrupted IFD: %s', ifd) return for i in range(entries): # entry is index of start of this IFD in the file entry = ifd + 2 + 12 * i tag = self.s2n(entry, 2) # get tag name early to avoid errors, help debug tag_entry = tag_dict.get(tag) if tag_entry: tag_name = tag_entry[0] else: tag_name = 'Tag 0x%04X' % tag # ignore certain tags for faster processing if not (not self.detailed and tag in IGNORE_TAGS): self._process_tag(ifd, ifd_name, tag_entry, entry, tag, tag_name, relative, stop_tag) if tag_name == stop_tag: break def extract_tiff_thumbnail(self, thumb_ifd: int) -> None: """ Extract uncompressed TIFF thumbnail. Take advantage of the pre-existing layout in the thumbnail IFD as much as possible """ thumb = self.tags.get('Thumbnail Compression') if not thumb or thumb.printable != 'Uncompressed TIFF': return entries = self.s2n(thumb_ifd, 2) # this is header plus offset to IFD ... if self.endian == 'M': tiff = b'MM\x00*\x00\x00\x00\x08' else: tiff = b'II*\x00\x08\x00\x00\x00' # ... plus thumbnail IFD data plus a null "next IFD" pointer self.file_handle.seek(self.offset + thumb_ifd) tiff += self.file_handle.read(entries * 12 + 2) + b'\x00\x00\x00\x00' # fix up large value offset pointers into data area for i in range(entries): entry = thumb_ifd + 2 + 12 * i tag = self.s2n(entry, 2) field_type = self.s2n(entry + 2, 2) type_length = FIELD_TYPES[field_type][0] count = self.s2n(entry + 4, 4) old_offset = self.s2n(entry + 8, 4) # start of the 4-byte pointer area in entry ptr = i * 12 + 18 # remember strip offsets location if tag == 0x0111: strip_off = ptr strip_len = count * type_length # is it in the data area? if count * type_length > 4: # update offset pointer (nasty "strings are immutable" crap) # should be able to say "tiff[ptr:ptr+4]=newoff" newoff = len(tiff) tiff = tiff[:ptr] + self.n2b(newoff, 4) + tiff[ptr + 4:] # remember strip offsets location if tag == 0x0111: strip_off = newoff strip_len = 4 # get original data and store it self.file_handle.seek(self.offset + old_offset) tiff += self.file_handle.read(count * type_length) # add pixel strips and update strip offset info old_offsets = self.tags['Thumbnail StripOffsets'].values old_counts = self.tags['Thumbnail StripByteCounts'].values for i, old_offset in enumerate(old_offsets): # update offset pointer (more nasty "strings are immutable" crap) offset = self.n2b(len(tiff), strip_len) tiff = tiff[:strip_off] + offset + tiff[strip_off + strip_len:] strip_off += strip_len # add pixel strip to end self.file_handle.seek(self.offset + old_offset) tiff += self.file_handle.read(old_counts[i]) self.tags['TIFFThumbnail'] = tiff def extract_jpeg_thumbnail(self) -> None: """ Extract JPEG thumbnail. (Thankfully the JPEG data is stored as a unit.) """ thumb_offset = self.tags.get('Thumbnail JPEGInterchangeFormat') if thumb_offset: self.file_handle.seek(self.offset + thumb_offset.values[0]) size = self.tags['Thumbnail JPEGInterchangeFormatLength'].values[0] self.tags['JPEGThumbnail'] = self.file_handle.read(size) # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote # since it's not allowed in a uncompressed TIFF IFD if 'JPEGThumbnail' not in self.tags: thumb_offset = self.tags.get('MakerNote JPEGThumbnail') if thumb_offset: self.file_handle.seek(self.offset + thumb_offset.values[0]) self.tags['JPEGThumbnail'] = self.file_handle.read(thumb_offset.field_length) def decode_maker_note(self) -> None: """ Decode all the camera-specific MakerNote formats Note is the data that comprises this MakerNote. The MakerNote will likely have pointers in it that point to other parts of the file. We'll use self.offset as the starting point for most of those pointers, since they are relative to the beginning of the file. If the MakerNote is in a newer format, it may use relative addressing within the MakerNote. In that case we'll use relative addresses for the pointers. As an aside: it's not just to be annoying that the manufacturers use relative offsets. It's so that if the makernote has to be moved by the picture software all of the offsets don't have to be adjusted. Overall, this is probably the right strategy for makernotes, though the spec is ambiguous. The spec does not appear to imagine that makernotes would follow EXIF format internally. Once they did, it's ambiguous whether the offsets should be from the header at the start of all the EXIF info, or from the header at the start of the makernote. TODO: look into splitting this up """ note = self.tags['EXIF MakerNote'] # Some apps use MakerNote tags but do not use a format for which we # have a description, so just do a raw dump for these. make = self.tags['Image Make'].printable # Nikon # The maker note usually starts with the word Nikon, followed by the # type of the makernote (1 or 2, as a short). If the word Nikon is # not at the start of the makernote, it's probably type 2, since some # cameras work that way. if 'NIKON' in make: if note.values[0:7] == [78, 105, 107, 111, 110, 0, 1]: logger.debug('Looks like a type 1 Nikon MakerNote.') self.dump_ifd(note.field_offset + 8, 'MakerNote', tag_dict=makernote.nikon.TAGS_OLD) elif note.values[0:7] == [78, 105, 107, 111, 110, 0, 2]: logger.debug('Looks like a labeled type 2 Nikon MakerNote') if note.values[12:14] != [0, 42] and note.values[12:14] != [42, 0]: raise ValueError('Missing marker tag 42 in MakerNote.') # skip the Makernote label and the TIFF header self.dump_ifd(note.field_offset + 10 + 8, 'MakerNote', tag_dict=makernote.nikon.TAGS_NEW, relative=1) else: # E99x or D1 logger.debug('Looks like an unlabeled type 2 Nikon MakerNote') self.dump_ifd(note.field_offset, 'MakerNote', tag_dict=makernote.nikon.TAGS_NEW) return # Olympus if make.startswith('OLYMPUS'): self.dump_ifd(note.field_offset + 8, 'MakerNote', tag_dict=makernote.olympus.TAGS) # TODO #for i in (('MakerNote Tag 0x2020', makernote.OLYMPUS_TAG_0x2020),): # self.decode_olympus_tag(self.tags[i[0]].values, i[1]) #return # Casio if 'CASIO' in make or 'Casio' in make: self.dump_ifd(note.field_offset, 'MakerNote', tag_dict=makernote.casio.TAGS) return # Fujifilm if make == 'FUJIFILM': # bug: everything else is "Motorola" endian, but the MakerNote # is "Intel" endian endian = self.endian self.endian = 'I' # bug: IFD offsets are from beginning of MakerNote, not # beginning of file header offset = self.offset self.offset += note.field_offset # process note with bogus values (note is actually at offset 12) self.dump_ifd(12, 'MakerNote', tag_dict=makernote.fujifilm.TAGS) # reset to correct values self.endian = endian self.offset = offset return # Apple if make == 'Apple' and note.values[0:10] == [65, 112, 112, 108, 101, 32, 105, 79, 83, 0]: offset = self.offset self.offset += note.field_offset + 14 self.dump_ifd(0, 'MakerNote', tag_dict=makernote.apple.TAGS) self.offset = offset return # Canon if make == 'Canon': self.dump_ifd(note.field_offset, 'MakerNote', tag_dict=makernote.canon.TAGS) for i in (('MakerNote Tag 0x0001', makernote.canon.CAMERA_SETTINGS), ('MakerNote Tag 0x0002', makernote.canon.FOCAL_LENGTH), ('MakerNote Tag 0x0004', makernote.canon.SHOT_INFO), ('MakerNote Tag 0x0026', makernote.canon.AF_INFO_2), ('MakerNote Tag 0x0093', makernote.canon.FILE_INFO)): if i[0] in self.tags: logger.debug('Canon %s', i[0]) self._canon_decode_tag(self.tags[i[0]].values, i[1]) del self.tags[i[0]] if makernote.canon.CAMERA_INFO_TAG_NAME in self.tags: tag = self.tags[makernote.canon.CAMERA_INFO_TAG_NAME] logger.debug('Canon CameraInfo') self._canon_decode_camera_info(tag) del self.tags[makernote.canon.CAMERA_INFO_TAG_NAME] return # TODO Decode Olympus MakerNote tag based on offset within tag. # def _olympus_decode_tag(self, value, mn_tags): # pass def _canon_decode_tag(self, value, mn_tags): """ Decode Canon MakerNote tag based on offset within tag. See http://www.burren.cx/david/canon.html by David Burren """ for i in range(1, len(value)): tag = mn_tags.get(i, ('Unknown', )) name = tag[0] if len(tag) > 1: val = tag[1].get(value[i], 'Unknown') else: val = value[i] try: logger.debug(" %s %s %s", i, name, hex(value[i])) except TypeError: logger.debug(" %s %s %s", i, name, value[i]) # It's not a real IFD Tag but we fake one to make everybody happy. # This will have a "proprietary" type self.tags['MakerNote ' + name] = IfdTag(str(val), 0, 0, val, 0, 0) def _canon_decode_camera_info(self, camera_info_tag): """ Decode the variable length encoded camera info section. """ model = self.tags.get('Image Model', None) if not model: return model = str(model.values) camera_info_tags = {} for (model_name_re, tag_desc) in makernote.canon.CAMERA_INFO_MODEL_MAP.items(): if re.search(model_name_re, model): camera_info_tags = tag_desc break else: return # We are assuming here that these are all unsigned bytes (Byte or # Unknown) if camera_info_tag.field_type not in (1, 7): return camera_info = struct.pack('<%dB' % len(camera_info_tag.values), *camera_info_tag.values) # Look for each data value and decode it appropriately. for offset, tag in camera_info_tags.items(): tag_format = tag[1] tag_size = struct.calcsize(tag_format) if len(camera_info) < offset + tag_size: continue packed_tag_value = camera_info[offset:offset + tag_size] tag_value = struct.unpack(tag_format, packed_tag_value)[0] tag_name = tag[0] if len(tag) > 2: if callable(tag[2]): tag_value = tag[2](tag_value) else: tag_value = tag[2].get(tag_value, tag_value) logger.debug(" %s %s", tag_name, tag_value) self.tags['MakerNote ' + tag_name] = IfdTag(str(tag_value), 0, 0, tag_value, 0, 0) def parse_xmp(self, xmp_bytes: bytes): """Adobe's Extensible Metadata Platform, just dump the pretty XML.""" import xml.dom.minidom # pylint: disable=import-outside-toplevel logger.debug("XMP cleaning data") # Pray that it's encoded in UTF-8 # TODO: allow user to specify encoding xmp_string = xmp_bytes.decode("utf-8") try: pretty = xml.dom.minidom.parseString(xmp_string).toprettyxml() except xml.parsers.expat.ExpatError: logger.warning("XMP: XML is not well formed") self.tags['Image ApplicationNotes'] = IfdTag(xmp_string, 0, 1, xmp_bytes, 0, 0) return cleaned = [] for line in pretty.splitlines(): if line.strip(): cleaned.append(line) self.tags['Image ApplicationNotes'] = IfdTag('\n'.join(cleaned), 0, 1, xmp_bytes, 0, 0) exif-py-3.0.0/exifread/exceptions.py000066400000000000000000000001211423577444100174220ustar00rootroot00000000000000class InvalidExif(Exception): pass class ExifNotFound(Exception): pass exif-py-3.0.0/exifread/exif_log.py000066400000000000000000000041241423577444100170440ustar00rootroot00000000000000""" Custom log output """ import sys import logging TEXT_NORMAL = 0 TEXT_BOLD = 1 TEXT_RED = 31 TEXT_GREEN = 32 TEXT_YELLOW = 33 TEXT_BLUE = 34 TEXT_MAGENTA = 35 TEXT_CYAN = 36 def get_logger(): """Use this from all files needing to log.""" return logging.getLogger("exifread") def setup_logger(debug, color): """Configure the logger.""" if debug: log_level = logging.DEBUG else: log_level = logging.INFO logger = logging.getLogger("exifread") stream = Handler(log_level, debug, color) logger.addHandler(stream) logger.setLevel(log_level) class Formatter(logging.Formatter): """ Custom formatter, we like colors! """ def __init__(self, debug=False, color=False): self.color = color self.debug = debug if self.debug: log_format = "%(levelname)-6s %(message)s" else: log_format = "%(message)s" logging.Formatter.__init__(self, log_format) def format(self, record): if self.debug and self.color: if record.levelno >= logging.CRITICAL: color = TEXT_RED elif record.levelno >= logging.ERROR: color = TEXT_RED elif record.levelno >= logging.WARNING: color = TEXT_YELLOW elif record.levelno >= logging.INFO: color = TEXT_GREEN elif record.levelno >= logging.DEBUG: color = TEXT_CYAN else: color = TEXT_NORMAL record.levelname = "\x1b[%sm%s\x1b[%sm" % (color, record.levelname, TEXT_NORMAL) return logging.Formatter.format(self, record) class Handler(logging.StreamHandler): def __init__(self, log_level, debug=False, color=False): self.color = color self.debug = debug logging.StreamHandler.__init__(self, sys.stdout) self.setFormatter(Formatter(debug, color)) self.setLevel(log_level) # def emit(self, record): # record.msg = "\x1b[%sm%s\x1b[%sm" % (TEXT_BOLD, record.msg, TEXT_NORMAL) # logging.StreamHandler.emit(self, record) exif-py-3.0.0/exifread/heic.py000066400000000000000000000217321423577444100161640ustar00rootroot00000000000000# Find Exif data in an HEIC file. # As of 2019, the latest standard seems to be "ISO/IEC 14496-12:2015" # There are many different related standards. (quicktime, mov, mp4, etc...) # See https://en.wikipedia.org/wiki/ISO_base_media_file_format for more details. # We parse just enough of the ISO format to locate the Exif data in the file. # Inside the 'meta' box are two directories we need: # 1) the 'iinf' box contains 'infe' records, we look for the item_id for 'Exif'. # 2) once we have the item_id, we find a matching entry in the 'iloc' box, which # gives us position and size information. import struct from typing import List, Dict, Callable, BinaryIO, Optional from exifread.exif_log import get_logger logger = get_logger() class WrongBox(Exception): pass class NoParser(Exception): pass class BoxVersion(Exception): pass class BadSize(Exception): pass class Box: version = 0 minor_version = 0 item_count = 0 size = 0 after = 0 pos = 0 compat = [] # type: List base_offset = 0 # this is full of boxes, but not in a predictable order. subs = {} # type: Dict[str, Box] locs = {} # type: Dict exif_infe = None # type: Optional[Box] item_id = 0 item_type = b'' item_name = b'' item_protection_index = 0 major_brand = b'' offset_size = 0 length_size = 0 base_offset_size = 0 index_size = 0 flags = 0 def __init__(self, name: str): self.name = name def __repr__(self) -> str: return "" % self.name def set_sizes(self, offset: int, length: int, base_offset: int, index: int): self.offset_size = offset self.length_size = length self.base_offset_size = base_offset self.index_size = index def set_full(self, vflags: int): """ ISO boxes come in 'old' and 'full' variants. The 'full' variant contains version and flags information. """ self.version = vflags >> 24 self.flags = vflags & 0x00ffffff class HEICExifFinder: def __init__(self, file_handle: BinaryIO): self.file_handle = file_handle def get(self, nbytes: int) -> bytes: read = self.file_handle.read(nbytes) if not read: raise EOFError if len(read) != nbytes: msg = "get(nbytes={nbytes}) found {read} bytes at position {pos}".format( nbytes=nbytes, read=len(read), pos=self.file_handle.tell() ) raise BadSize(msg) return read def get16(self) -> int: return struct.unpack('>H', self.get(2))[0] def get32(self) -> int: return struct.unpack('>L', self.get(4))[0] def get64(self) -> int: return struct.unpack('>Q', self.get(8))[0] def get_int4x2(self) -> tuple: num = struct.unpack('>B', self.get(1))[0] num0 = num >> 4 num1 = num & 0xf return num0, num1 def get_int(self, size: int) -> int: """some fields have variant-sized data.""" if size == 2: return self.get16() if size == 4: return self.get32() if size == 8: return self.get64() if size == 0: return 0 raise BadSize(size) def get_string(self) -> bytes: read = [] while 1: char = self.get(1) if char == b'\x00': break read.append(char) return b''.join(read) def next_box(self) -> Box: pos = self.file_handle.tell() size = self.get32() kind = self.get(4).decode('ascii') box = Box(kind) if size == 0: # signifies 'to the end of the file', we shouldn't see this. raise NotImplementedError if size == 1: # 64-bit size follows type. size = self.get64() box.size = size - 16 box.after = pos + size else: box.size = size - 8 box.after = pos + size box.pos = self.file_handle.tell() return box def get_full(self, box: Box): box.set_full(self.get32()) def skip(self, box: Box): self.file_handle.seek(box.after) def expect_parse(self, name: str) -> Box: while True: box = self.next_box() if box.name == name: return self.parse_box(box) self.skip(box) def get_parser(self, box: Box) -> Callable: defs = { 'ftyp': self._parse_ftyp, 'meta': self._parse_meta, 'infe': self._parse_infe, 'iinf': self._parse_iinf, 'iloc': self._parse_iloc, } try: return defs[box.name] except (IndexError, KeyError) as err: raise NoParser(box.name) from err def parse_box(self, box: Box) -> Box: probe = self.get_parser(box) probe(box) # in case anything is left unread self.file_handle.seek(box.after) return box def _parse_ftyp(self, box: Box): box.major_brand = self.get(4) box.minor_version = self.get32() box.compat = [] size = box.size - 8 while size > 0: box.compat.append(self.get(4)) size -= 4 def _parse_meta(self, meta: Box): self.get_full(meta) while self.file_handle.tell() < meta.after: box = self.next_box() psub = self.get_parser(box) if psub is not None: psub(box) meta.subs[box.name] = box else: logger.debug('HEIC: skipping %r', box) # skip any unparsed data self.skip(box) def _parse_infe(self, box: Box): self.get_full(box) if box.version >= 2: if box.version == 2: box.item_id = self.get16() elif box.version == 3: box.item_id = self.get32() box.item_protection_index = self.get16() box.item_type = self.get(4) box.item_name = self.get_string() # ignore the rest def _parse_iinf(self, box: Box): self.get_full(box) count = self.get16() box.exif_infe = None for _ in range(count): infe = self.expect_parse('infe') if infe.item_type == b'Exif': logger.debug("HEIC: found Exif 'infe' box") box.exif_infe = infe break def _parse_iloc(self, box: Box): self.get_full(box) size0, size1 = self.get_int4x2() size2, size3 = self.get_int4x2() box.set_sizes(size0, size1, size2, size3) if box.version < 2: box.item_count = self.get16() elif box.version == 2: box.item_count = self.get32() else: raise BoxVersion(2, box.version) box.locs = {} logger.debug('HEIC: %d iloc items', box.item_count) for _ in range(box.item_count): if box.version < 2: item_id = self.get16() elif box.version == 2: item_id = self.get32() else: # notreached raise BoxVersion(2, box.version) if box.version in (1, 2): # ignore construction_method self.get16() # ignore data_reference_index self.get16() box.base_offset = self.get_int(box.base_offset_size) extent_count = self.get16() extents = [] for _ in range(extent_count): if box.version in (1, 2) and box.index_size > 0: self.get_int(box.index_size) extent_offset = self.get_int(box.offset_size) extent_length = self.get_int(box.length_size) extents.append((extent_offset, extent_length)) box.locs[item_id] = extents def find_exif(self) -> tuple: ftyp = self.expect_parse('ftyp') assert ftyp.major_brand == b'heic' assert ftyp.minor_version == 0 meta = self.expect_parse('meta') assert meta.subs['iinf'].exif_infe is not None item_id = meta.subs['iinf'].exif_infe.item_id extents = meta.subs['iloc'].locs[item_id] logger.debug('HEIC: found Exif location.') # we expect the Exif data to be in one piece. assert len(extents) == 1 pos, _ = extents[0] # looks like there's a kind of pseudo-box here. self.file_handle.seek(pos) # the payload of "Exif" item may be start with either # b'\xFF\xE1\xSS\xSSExif\x00\x00' (with APP1 marker, e.g. Android Q) # or # b'Exif\x00\x00' (without APP1 marker, e.g. iOS) # according to "ISO/IEC 23008-12, 2017-12", both of them are legal exif_tiff_header_offset = self.get32() assert exif_tiff_header_offset >= 6 assert self.get(exif_tiff_header_offset)[-6:] == b'Exif\x00\x00' offset = self.file_handle.tell() endian = self.file_handle.read(1) return offset, endian exif-py-3.0.0/exifread/jpeg.py000066400000000000000000000153371423577444100162050ustar00rootroot00000000000000from typing import BinaryIO from exifread.utils import ord_ from exifread.exif_log import get_logger from exifread.exceptions import InvalidExif logger = get_logger() def _increment_base(data, base): return ord_(data[base + 2]) * 256 + ord_(data[base + 3]) + 2 def _get_initial_base(fh: BinaryIO, data, fake_exif) -> tuple: base = 2 logger.debug("data[2]=0x%X data[3]=0x%X data[6:10]=%s", ord_(data[2]), ord_(data[3]), data[6:10]) while ord_(data[2]) == 0xFF and data[6:10] in (b"JFIF", b"JFXX", b"OLYM", b"Phot"): length = ord_(data[4]) * 256 + ord_(data[5]) logger.debug(" Length offset is %s", length) fh.read(length - 8) # fake an EXIF beginning of file # I don't think this is used. --gd data = b"\xFF\x00" + fh.read(10) fake_exif = 1 if base > 2: logger.debug(" Added to base") base = base + length + 4 - 2 else: logger.debug(" Added to zero") base = length + 4 logger.debug(" Set segment base to 0x%X", base) return base, fake_exif def _get_base(base, data) -> int: # pylint: disable=too-many-statements while True: logger.debug(" Segment base 0x%X", base) if data[base : base + 2] == b"\xFF\xE1": # APP1 logger.debug(" APP1 at base 0x%X", base) logger.debug(" Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3])) logger.debug(" Code: %s", data[base + 4 : base + 8]) if data[base + 4 : base + 8] == b"Exif": logger.debug(" Decrement base by 2 to get to pre-segment header (for compatibility with later code)") base -= 2 break increment = _increment_base(data, base) logger.debug(" Increment base by %s", increment) base += increment elif data[base : base + 2] == b"\xFF\xE0": # APP0 logger.debug(" APP0 at base 0x%X", base) logger.debug(" Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3])) logger.debug(" Code: %s", data[base + 4 : base + 8]) increment = _increment_base(data, base) logger.debug(" Increment base by %s", increment) base += increment elif data[base : base + 2] == b"\xFF\xE2": # APP2 logger.debug(" APP2 at base 0x%X", base) logger.debug(" Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3])) logger.debug(" Code: %s", data[base + 4 : base + 8]) increment = _increment_base(data, base) logger.debug(" Increment base by %s", increment) base += increment elif data[base : base + 2] == b"\xFF\xEE": # APP14 logger.debug(" APP14 Adobe segment at base 0x%X", base) logger.debug(" Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3])) logger.debug(" Code: %s", data[base + 4 : base + 8]) increment = _increment_base(data, base) logger.debug(" Increment base by %s", increment) base += increment logger.debug(" There is useful EXIF-like data here, but we have no parser for it.") elif data[base : base + 2] == b"\xFF\xDB": logger.debug(" JPEG image data at base 0x%X No more segments are expected.", base) break elif data[base : base + 2] == b"\xFF\xD8": # APP12 logger.debug(" FFD8 segment at base 0x%X", base) logger.debug( " Got 0x%X 0x%X and %s instead", ord_(data[base]), ord_(data[base + 1]), data[4 + base : 10 + base] ) logger.debug(" Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3])) logger.debug(" Code: %s", data[base + 4 : base + 8]) increment = _increment_base(data, base) logger.debug(" Increment base by %s", increment) base += increment elif data[base : base + 2] == b"\xFF\xEC": # APP12 logger.debug(" APP12 XMP (Ducky) or Pictureinfo segment at base 0x%X", base) logger.debug(" Got 0x%X and 0x%X instead", ord_(data[base]), ord_(data[base + 1])) logger.debug(" Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3])) logger.debug("Code: %s", data[base + 4 : base + 8]) increment = _increment_base(data, base) logger.debug(" Increment base by %s", increment) base += increment logger.debug( " There is useful EXIF-like data here (quality, comment, copyright), " "but we have no parser for it." ) else: try: increment = _increment_base(data, base) logger.debug(" Got 0x%X and 0x%X instead", ord_(data[base]), ord_(data[base + 1])) except IndexError as err: raise InvalidExif("Unexpected/unhandled segment type or file content.") from err else: logger.debug(" Increment base by %s", increment) base += increment return base def find_jpeg_exif(fh: BinaryIO, data, fake_exif) -> tuple: logger.debug("JPEG format recognized data[0:2]=0x%X%X", ord_(data[0]), ord_(data[1])) base, fake_exif = _get_initial_base(fh, data, fake_exif) # Big ugly patch to deal with APP2 (or other) data coming before APP1 fh.seek(0) # in theory, this could be insufficient since 64K is the maximum size--gd data = fh.read(base + 4000) base = _get_base(base, data) fh.seek(base + 12) if ord_(data[2 + base]) == 0xFF and data[6 + base : 10 + base] == b"Exif": # detected EXIF header offset = fh.tell() endian = fh.read(1) # HACK TEST: endian = 'M' elif ord_(data[2 + base]) == 0xFF and data[6 + base : 10 + base + 1] == b"Ducky": # detected Ducky header. logger.debug( "EXIF-like header (normally 0xFF and code): 0x%X and %s", ord_(data[2 + base]), data[6 + base : 10 + base + 1], ) offset = fh.tell() endian = fh.read(1) elif ord_(data[2 + base]) == 0xFF and data[6 + base : 10 + base + 1] == b"Adobe": # detected APP14 (Adobe) logger.debug( "EXIF-like header (normally 0xFF and code): 0x%X and %s", ord_(data[2 + base]), data[6 + base : 10 + base + 1], ) offset = fh.tell() endian = fh.read(1) else: # no EXIF information msg = "No EXIF header expected data[2+base]==0xFF and data[6+base:10+base]===Exif (or Duck)" msg += "Did get 0x%X and %s" % (ord_(data[2 + base]), data[6 + base : 10 + base + 1]) raise InvalidExif(msg) return offset, endian, fake_exif exif-py-3.0.0/exifread/tags/000077500000000000000000000000001423577444100156335ustar00rootroot00000000000000exif-py-3.0.0/exifread/tags/__init__.py000066400000000000000000000015241423577444100177460ustar00rootroot00000000000000""" Tag definitions """ from exifread.tags.exif import EXIF_TAGS from exifread.tags.makernote import ( apple, canon, casio, fujifilm, nikon, olympus, ) DEFAULT_STOP_TAG = 'UNDEF' # field type descriptions as (length, abbreviation, full name) tuples FIELD_TYPES = ( (0, 'X', 'Proprietary'), # no such type (1, 'B', 'Byte'), (1, 'A', 'ASCII'), (2, 'S', 'Short'), (4, 'L', 'Long'), (8, 'R', 'Ratio'), (1, 'SB', 'Signed Byte'), (1, 'U', 'Undefined'), (2, 'SS', 'Signed Short'), (4, 'SL', 'Signed Long'), (8, 'SR', 'Signed Ratio'), (4, 'F32', 'Single-Precision Floating Point (32-bit)'), (8, 'F64', 'Double-Precision Floating Point (64-bit)'), (4, 'L', 'IFD'), ) # To ignore when quick processing IGNORE_TAGS = ( 0x02BC, # XPM 0x927C, # MakerNote Tags 0x9286, # user comment ) exif-py-3.0.0/exifread/tags/exif.py000066400000000000000000000335501423577444100171460ustar00rootroot00000000000000""" Standard tag definitions. """ from exifread.utils import make_string, make_string_uc # Interoperability tags INTEROP_TAGS = { 0x0001: ('InteroperabilityIndex', ), 0x0002: ('InteroperabilityVersion', ), 0x1000: ('RelatedImageFileFormat', ), 0x1001: ('RelatedImageWidth', ), 0x1002: ('RelatedImageLength', ), } INTEROP_INFO = ( 'Interoperability', INTEROP_TAGS ) # GPS tags GPS_TAGS = { 0x0000: ('GPSVersionID', ), 0x0001: ('GPSLatitudeRef', ), 0x0002: ('GPSLatitude', ), 0x0003: ('GPSLongitudeRef', ), 0x0004: ('GPSLongitude', ), 0x0005: ('GPSAltitudeRef', ), 0x0006: ('GPSAltitude', ), 0x0007: ('GPSTimeStamp', ), 0x0008: ('GPSSatellites', ), 0x0009: ('GPSStatus', ), 0x000A: ('GPSMeasureMode', ), 0x000B: ('GPSDOP', ), 0x000C: ('GPSSpeedRef', ), 0x000D: ('GPSSpeed', ), 0x000E: ('GPSTrackRef', ), 0x000F: ('GPSTrack', ), 0x0010: ('GPSImgDirectionRef', ), 0x0011: ('GPSImgDirection', ), 0x0012: ('GPSMapDatum', ), 0x0013: ('GPSDestLatitudeRef', ), 0x0014: ('GPSDestLatitude', ), 0x0015: ('GPSDestLongitudeRef', ), 0x0016: ('GPSDestLongitude', ), 0x0017: ('GPSDestBearingRef', ), 0x0018: ('GPSDestBearing', ), 0x0019: ('GPSDestDistanceRef', ), 0x001A: ('GPSDestDistance', ), 0x001B: ('GPSProcessingMethod', ), 0x001C: ('GPSAreaInformation', ), 0x001D: ('GPSDate', ), 0x001E: ('GPSDifferential', ), } GPS_INFO = ( 'GPS', GPS_TAGS ) # Main Exif tag names EXIF_TAGS = { 0x00FE: ('SubfileType', { 0x0: 'Full-resolution Image', 0x1: 'Reduced-resolution image', 0x2: 'Single page of multi-page image', 0x3: 'Single page of multi-page reduced-resolution image', 0x4: 'Transparency mask', 0x5: 'Transparency mask of reduced-resolution image', 0x6: 'Transparency mask of multi-page image', 0x7: 'Transparency mask of reduced-resolution multi-page image', 0x10001: 'Alternate reduced-resolution image', 0xffffffff: 'invalid ', }), 0x00FF: ('OldSubfileType', { 1: 'Full-resolution image', 2: 'Reduced-resolution image', 3: 'Single page of multi-page image', }), 0x0100: ('ImageWidth', ), 0x0101: ('ImageLength', ), 0x0102: ('BitsPerSample', ), 0x0103: ('Compression', { 1: 'Uncompressed', 2: 'CCITT 1D', 3: 'T4/Group 3 Fax', 4: 'T6/Group 4 Fax', 5: 'LZW', 6: 'JPEG (old-style)', 7: 'JPEG', 8: 'Adobe Deflate', 9: 'JBIG B&W', 10: 'JBIG Color', 32766: 'Next', 32769: 'Epson ERF Compressed', 32771: 'CCIRLEW', 32773: 'PackBits', 32809: 'Thunderscan', 32895: 'IT8CTPAD', 32896: 'IT8LW', 32897: 'IT8MP', 32898: 'IT8BL', 32908: 'PixarFilm', 32909: 'PixarLog', 32946: 'Deflate', 32947: 'DCS', 34661: 'JBIG', 34676: 'SGILog', 34677: 'SGILog24', 34712: 'JPEG 2000', 34713: 'Nikon NEF Compressed', 65000: 'Kodak DCR Compressed', 65535: 'Pentax PEF Compressed' }), 0x0106: ('PhotometricInterpretation', ), 0x0107: ('Thresholding', ), 0x0108: ('CellWidth', ), 0x0109: ('CellLength', ), 0x010A: ('FillOrder', ), 0x010D: ('DocumentName', ), 0x010E: ('ImageDescription', ), 0x010F: ('Make', ), 0x0110: ('Model', ), 0x0111: ('StripOffsets', ), 0x0112: ('Orientation', { 1: 'Horizontal (normal)', 2: 'Mirrored horizontal', 3: 'Rotated 180', 4: 'Mirrored vertical', 5: 'Mirrored horizontal then rotated 90 CCW', 6: 'Rotated 90 CW', 7: 'Mirrored horizontal then rotated 90 CW', 8: 'Rotated 90 CCW' }), 0x0115: ('SamplesPerPixel', ), 0x0116: ('RowsPerStrip', ), 0x0117: ('StripByteCounts', ), 0x0118: ('MinSampleValue', ), 0x0119: ('MaxSampleValue', ), 0x011A: ('XResolution', ), 0x011B: ('YResolution', ), 0x011C: ('PlanarConfiguration', ), 0x011D: ('PageName', make_string), 0x011E: ('XPosition', ), 0x011F: ('YPosition', ), 0x0122: ('GrayResponseUnit', { 1: '0.1', 2: '0.001', 3: '0.0001', 4: '1e-05', 5: '1e-06', }), 0x0123: ('GrayResponseCurve', ), 0x0124: ('T4Options', ), 0x0125: ('T6Options', ), 0x0128: ('ResolutionUnit', { 1: 'Not Absolute', 2: 'Pixels/Inch', 3: 'Pixels/Centimeter' }), 0x0129: ('PageNumber', ), 0x012C: ('ColorResponseUnit', ), 0x012D: ('TransferFunction', ), 0x0131: ('Software', ), 0x0132: ('DateTime', ), 0x013B: ('Artist', ), 0x013C: ('HostComputer', ), 0x013D: ('Predictor', { 1: 'None', 2: 'Horizontal differencing' }), 0x013E: ('WhitePoint', ), 0x013F: ('PrimaryChromaticities', ), 0x0140: ('ColorMap', ), 0x0141: ('HalftoneHints', ), 0x0142: ('TileWidth', ), 0x0143: ('TileLength', ), 0x0144: ('TileOffsets', ), 0x0145: ('TileByteCounts', ), 0x0146: ('BadFaxLines', ), 0x0147: ('CleanFaxData', { 0: 'Clean', 1: 'Regenerated', 2: 'Unclean' }), 0x014A: ('SubIFDs', ), 0x0148: ('ConsecutiveBadFaxLines', ), 0x014C: ('InkSet', { 1: 'CMYK', 2: 'Not CMYK' }), 0x014D: ('InkNames', ), 0x014E: ('NumberofInks', ), 0x0150: ('DotRange', ), 0x0151: ('TargetPrinter', ), 0x0152: ('ExtraSamples', { 0: 'Unspecified', 1: 'Associated Alpha', 2: 'Unassociated Alpha' }), 0x0153: ('SampleFormat', { 1: 'Unsigned', 2: 'Signed', 3: 'Float', 4: 'Undefined', 5: 'Complex int', 6: 'Complex float' }), 0x0154: ('SMinSampleValue', ), 0x0155: ('SMaxSampleValue', ), 0x0156: ('TransferRange', ), 0x0157: ('ClipPath', ), 0x015B: ('JPEGTables', ), 0x0200: ('JPEGProc', ), 0x0201: ('JPEGInterchangeFormat', ), # JpegIFOffset 0x0202: ('JPEGInterchangeFormatLength', ), # JpegIFByteCount 0x0211: ('YCbCrCoefficients', ), 0x0212: ('YCbCrSubSampling', ), 0x0213: ('YCbCrPositioning', { 1: 'Centered', 2: 'Co-sited' }), 0x0214: ('ReferenceBlackWhite', ), 0x02BC: ('ApplicationNotes', ), # XPM Info 0x4746: ('Rating', ), 0x828D: ('CFARepeatPatternDim', ), 0x828E: ('CFAPattern', ), 0x828F: ('BatteryLevel', ), 0x8298: ('Copyright', ), 0x829A: ('ExposureTime', ), 0x829D: ('FNumber', ), 0x83BB: ('IPTC/NAA', ), 0x8769: ('ExifOffset', ), # Exif Tags 0x8773: ('InterColorProfile', ), 0x8822: ('ExposureProgram', { 0: 'Unidentified', 1: 'Manual', 2: 'Program Normal', 3: 'Aperture Priority', 4: 'Shutter Priority', 5: 'Program Creative', 6: 'Program Action', 7: 'Portrait Mode', 8: 'Landscape Mode' }), 0x8824: ('SpectralSensitivity', ), 0x8825: ('GPSInfo', GPS_INFO), # GPS tags 0x8827: ('ISOSpeedRatings', ), 0x8828: ('OECF', ), 0x8829: ('Interlace', ), 0x882A: ('TimeZoneOffset', ), 0x882B: ('SelfTimerMode', ), 0x8830: ('SensitivityType', { 0: 'Unknown', 1: 'Standard Output Sensitivity', 2: 'Recommended Exposure Index', 3: 'ISO Speed', 4: 'Standard Output Sensitivity and Recommended Exposure Index', 5: 'Standard Output Sensitivity and ISO Speed', 6: 'Recommended Exposure Index and ISO Speed', 7: 'Standard Output Sensitivity, Recommended Exposure Index and ISO Speed' }), 0x8832: ('RecommendedExposureIndex', ), 0x8833: ('ISOSpeed', ), 0x9000: ('ExifVersion', make_string), 0x9003: ('DateTimeOriginal', ), 0x9004: ('DateTimeDigitized', ), 0x9010: ('OffsetTime', ), 0x9011: ('OffsetTimeOriginal', ), 0x9012: ('OffsetTimeDigitized', ), 0x9101: ('ComponentsConfiguration', { 0: '', 1: 'Y', 2: 'Cb', 3: 'Cr', 4: 'Red', 5: 'Green', 6: 'Blue' }), 0x9102: ('CompressedBitsPerPixel', ), 0x9201: ('ShutterSpeedValue', ), 0x9202: ('ApertureValue', ), 0x9203: ('BrightnessValue', ), 0x9204: ('ExposureBiasValue', ), 0x9205: ('MaxApertureValue', ), 0x9206: ('SubjectDistance', ), 0x9207: ('MeteringMode', { 0: 'Unidentified', 1: 'Average', 2: 'CenterWeightedAverage', 3: 'Spot', 4: 'MultiSpot', 5: 'Pattern', 6: 'Partial', 255: 'other' }), 0x9208: ('LightSource', { 0: 'Unknown', 1: 'Daylight', 2: 'Fluorescent', 3: 'Tungsten (incandescent light)', 4: 'Flash', 9: 'Fine weather', 10: 'Cloudy weather', 11: 'Shade', 12: 'Daylight fluorescent (D 5700 - 7100K)', 13: 'Day white fluorescent (N 4600 - 5400K)', 14: 'Cool white fluorescent (W 3900 - 4500K)', 15: 'White fluorescent (WW 3200 - 3700K)', 17: 'Standard light A', 18: 'Standard light B', 19: 'Standard light C', 20: 'D55', 21: 'D65', 22: 'D75', 23: 'D50', 24: 'ISO studio tungsten', 255: 'other light source' }), 0x9209: ('Flash', { 0: 'Flash did not fire', 1: 'Flash fired', 5: 'Strobe return light not detected', 7: 'Strobe return light detected', 9: 'Flash fired, compulsory flash mode', 13: 'Flash fired, compulsory flash mode, return light not detected', 15: 'Flash fired, compulsory flash mode, return light detected', 16: 'Flash did not fire, compulsory flash mode', 24: 'Flash did not fire, auto mode', 25: 'Flash fired, auto mode', 29: 'Flash fired, auto mode, return light not detected', 31: 'Flash fired, auto mode, return light detected', 32: 'No flash function', 65: 'Flash fired, red-eye reduction mode', 69: 'Flash fired, red-eye reduction mode, return light not detected', 71: 'Flash fired, red-eye reduction mode, return light detected', 73: 'Flash fired, compulsory flash mode, red-eye reduction mode', 77: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected', 79: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected', 89: 'Flash fired, auto mode, red-eye reduction mode', 93: 'Flash fired, auto mode, return light not detected, red-eye reduction mode', 95: 'Flash fired, auto mode, return light detected, red-eye reduction mode' }), 0x920A: ('FocalLength', ), 0x920B: ('FlashEnergy', ), 0x920C: ('SpatialFrequencyResponse', ), 0x920D: ('Noise', ), 0x9211: ('ImageNumber', ), 0x9212: ('SecurityClassification', ), 0x9213: ('ImageHistory', ), 0x9214: ('SubjectArea', ), 0x9215: ('ExposureIndex', ), 0x9216: ('TIFF/EPStandardID', ), 0x927C: ('MakerNote', ), 0x9286: ('UserComment', make_string_uc), 0x9290: ('SubSecTime', ), 0x9291: ('SubSecTimeOriginal', ), 0x9292: ('SubSecTimeDigitized', ), # used by Windows Explorer 0x9C9B: ('XPTitle', ), 0x9C9C: ('XPComment', ), 0x9C9D: ('XPAuthor', make_string), # (ignored by Windows Explorer if Artist exists) 0x9C9E: ('XPKeywords', ), 0x9C9F: ('XPSubject', ), 0xA000: ('FlashPixVersion', make_string), 0xA001: ('ColorSpace', { 1: 'sRGB', 2: 'Adobe RGB', 65535: 'Uncalibrated' }), 0xA002: ('ExifImageWidth', ), 0xA003: ('ExifImageLength', ), 0xA004: ('RelatedSoundFile', ), 0xA005: ('InteroperabilityOffset', INTEROP_INFO), 0xA20B: ('FlashEnergy', ), # 0x920B in TIFF/EP 0xA20C: ('SpatialFrequencyResponse', ), # 0x920C 0xA20E: ('FocalPlaneXResolution', ), # 0x920E 0xA20F: ('FocalPlaneYResolution', ), # 0x920F 0xA210: ('FocalPlaneResolutionUnit', ), # 0x9210 0xA214: ('SubjectLocation', ), # 0x9214 0xA215: ('ExposureIndex', ), # 0x9215 0xA217: ('SensingMethod', { # 0x9217 1: 'Not defined', 2: 'One-chip color area', 3: 'Two-chip color area', 4: 'Three-chip color area', 5: 'Color sequential area', 7: 'Trilinear', 8: 'Color sequential linear' }), 0xA300: ('FileSource', { 1: 'Film Scanner', 2: 'Reflection Print Scanner', 3: 'Digital Camera' }), 0xA301: ('SceneType', { 1: 'Directly Photographed' }), 0xA302: ('CVAPattern', ), 0xA401: ('CustomRendered', { 0: 'Normal', 1: 'Custom' }), 0xA402: ('ExposureMode', { 0: 'Auto Exposure', 1: 'Manual Exposure', 2: 'Auto Bracket' }), 0xA403: ('WhiteBalance', { 0: 'Auto', 1: 'Manual' }), 0xA404: ('DigitalZoomRatio', ), 0xA405: ('FocalLengthIn35mmFilm', ), 0xA406: ('SceneCaptureType', { 0: 'Standard', 1: 'Landscape', 2: 'Portrait', 3: 'Night' }), 0xA407: ('GainControl', { 0: 'None', 1: 'Low gain up', 2: 'High gain up', 3: 'Low gain down', 4: 'High gain down' }), 0xA408: ('Contrast', { 0: 'Normal', 1: 'Soft', 2: 'Hard' }), 0xA409: ('Saturation', { 0: 'Normal', 1: 'Soft', 2: 'Hard' }), 0xA40A: ('Sharpness', { 0: 'Normal', 1: 'Soft', 2: 'Hard' }), 0xA40B: ('DeviceSettingDescription', ), 0xA40C: ('SubjectDistanceRange', ), 0xA420: ('ImageUniqueID', ), 0xA430: ('CameraOwnerName', ), 0xA431: ('BodySerialNumber', ), 0xA432: ('LensSpecification', ), 0xA433: ('LensMake', ), 0xA434: ('LensModel', ), 0xA435: ('LensSerialNumber', ), 0xA500: ('Gamma', ), 0xC4A5: ('PrintIM', ), 0xC61A: ('BlackLevel', ), 0xEA1C: ('Padding', ), 0xEA1D: ('OffsetSchema', ), 0xFDE8: ('OwnerName', ), 0xFDE9: ('SerialNumber', ), } exif-py-3.0.0/exifread/tags/makernote/000077500000000000000000000000001423577444100176205ustar00rootroot00000000000000exif-py-3.0.0/exifread/tags/makernote/__init__.py000066400000000000000000000000431423577444100217260ustar00rootroot00000000000000""" Makernote tag definitions. """ exif-py-3.0.0/exifread/tags/makernote/apple.py000066400000000000000000000004161423577444100212740ustar00rootroot00000000000000""" Makernote (proprietary) tag definitions for Apple iOS Based on version 1.01 of ExifTool -> Image/ExifTool/Apple.pm http://owl.phy.queensu.ca/~phil/exiftool/ """ TAGS = { 0x000a: ('HDRImageType', { 3: 'HDR Image', 4: 'Original Image', }), } exif-py-3.0.0/exifread/tags/makernote/canon.py000066400000000000000000000545731423577444100213060ustar00rootroot00000000000000""" Makernote (proprietary) tag definitions for Canon. http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Canon.html """ TAGS = { 0x0003: ('FlashInfo',), 0x0006: ('ImageType', ), 0x0007: ('FirmwareVersion', ), 0x0008: ('ImageNumber', ), 0x0009: ('OwnerName', ), 0x000c: ('SerialNumber', ), 0x000e: ('FileLength', ), 0x0010: ('ModelID', { 0x1010000: 'PowerShot A30', 0x1040000: 'PowerShot S300 / Digital IXUS 300 / IXY Digital 300', 0x1060000: 'PowerShot A20', 0x1080000: 'PowerShot A10', 0x1090000: 'PowerShot S110 / Digital IXUS v / IXY Digital 200', 0x1100000: 'PowerShot G2', 0x1110000: 'PowerShot S40', 0x1120000: 'PowerShot S30', 0x1130000: 'PowerShot A40', 0x1140000: 'EOS D30', 0x1150000: 'PowerShot A100', 0x1160000: 'PowerShot S200 / Digital IXUS v2 / IXY Digital 200a', 0x1170000: 'PowerShot A200', 0x1180000: 'PowerShot S330 / Digital IXUS 330 / IXY Digital 300a', 0x1190000: 'PowerShot G3', 0x1210000: 'PowerShot S45', 0x1230000: 'PowerShot SD100 / Digital IXUS II / IXY Digital 30', 0x1240000: 'PowerShot S230 / Digital IXUS v3 / IXY Digital 320', 0x1250000: 'PowerShot A70', 0x1260000: 'PowerShot A60', 0x1270000: 'PowerShot S400 / Digital IXUS 400 / IXY Digital 400', 0x1290000: 'PowerShot G5', 0x1300000: 'PowerShot A300', 0x1310000: 'PowerShot S50', 0x1340000: 'PowerShot A80', 0x1350000: 'PowerShot SD10 / Digital IXUS i / IXY Digital L', 0x1360000: 'PowerShot S1 IS', 0x1370000: 'PowerShot Pro1', 0x1380000: 'PowerShot S70', 0x1390000: 'PowerShot S60', 0x1400000: 'PowerShot G6', 0x1410000: 'PowerShot S500 / Digital IXUS 500 / IXY Digital 500', 0x1420000: 'PowerShot A75', 0x1440000: 'PowerShot SD110 / Digital IXUS IIs / IXY Digital 30a', 0x1450000: 'PowerShot A400', 0x1470000: 'PowerShot A310', 0x1490000: 'PowerShot A85', 0x1520000: 'PowerShot S410 / Digital IXUS 430 / IXY Digital 450', 0x1530000: 'PowerShot A95', 0x1540000: 'PowerShot SD300 / Digital IXUS 40 / IXY Digital 50', 0x1550000: 'PowerShot SD200 / Digital IXUS 30 / IXY Digital 40', 0x1560000: 'PowerShot A520', 0x1570000: 'PowerShot A510', 0x1590000: 'PowerShot SD20 / Digital IXUS i5 / IXY Digital L2', 0x1640000: 'PowerShot S2 IS', 0x1650000: 'PowerShot SD430 / Digital IXUS Wireless / IXY Digital Wireless', 0x1660000: 'PowerShot SD500 / Digital IXUS 700 / IXY Digital 600', 0x1668000: 'EOS D60', 0x1700000: 'PowerShot SD30 / Digital IXUS i Zoom / IXY Digital L3', 0x1740000: 'PowerShot A430', 0x1750000: 'PowerShot A410', 0x1760000: 'PowerShot S80', 0x1780000: 'PowerShot A620', 0x1790000: 'PowerShot A610', 0x1800000: 'PowerShot SD630 / Digital IXUS 65 / IXY Digital 80', 0x1810000: 'PowerShot SD450 / Digital IXUS 55 / IXY Digital 60', 0x1820000: 'PowerShot TX1', 0x1870000: 'PowerShot SD400 / Digital IXUS 50 / IXY Digital 55', 0x1880000: 'PowerShot A420', 0x1890000: 'PowerShot SD900 / Digital IXUS 900 Ti / IXY Digital 1000', 0x1900000: 'PowerShot SD550 / Digital IXUS 750 / IXY Digital 700', 0x1920000: 'PowerShot A700', 0x1940000: 'PowerShot SD700 IS / Digital IXUS 800 IS / IXY Digital 800 IS', 0x1950000: 'PowerShot S3 IS', 0x1960000: 'PowerShot A540', 0x1970000: 'PowerShot SD600 / Digital IXUS 60 / IXY Digital 70', 0x1980000: 'PowerShot G7', 0x1990000: 'PowerShot A530', 0x2000000: 'PowerShot SD800 IS / Digital IXUS 850 IS / IXY Digital 900 IS', 0x2010000: 'PowerShot SD40 / Digital IXUS i7 / IXY Digital L4', 0x2020000: 'PowerShot A710 IS', 0x2030000: 'PowerShot A640', 0x2040000: 'PowerShot A630', 0x2090000: 'PowerShot S5 IS', 0x2100000: 'PowerShot A460', 0x2120000: 'PowerShot SD850 IS / Digital IXUS 950 IS / IXY Digital 810 IS', 0x2130000: 'PowerShot A570 IS', 0x2140000: 'PowerShot A560', 0x2150000: 'PowerShot SD750 / Digital IXUS 75 / IXY Digital 90', 0x2160000: 'PowerShot SD1000 / Digital IXUS 70 / IXY Digital 10', 0x2180000: 'PowerShot A550', 0x2190000: 'PowerShot A450', 0x2230000: 'PowerShot G9', 0x2240000: 'PowerShot A650 IS', 0x2260000: 'PowerShot A720 IS', 0x2290000: 'PowerShot SX100 IS', 0x2300000: 'PowerShot SD950 IS / Digital IXUS 960 IS / IXY Digital 2000 IS', 0x2310000: 'PowerShot SD870 IS / Digital IXUS 860 IS / IXY Digital 910 IS', 0x2320000: 'PowerShot SD890 IS / Digital IXUS 970 IS / IXY Digital 820 IS', 0x2360000: 'PowerShot SD790 IS / Digital IXUS 90 IS / IXY Digital 95 IS', 0x2370000: 'PowerShot SD770 IS / Digital IXUS 85 IS / IXY Digital 25 IS', 0x2380000: 'PowerShot A590 IS', 0x2390000: 'PowerShot A580', 0x2420000: 'PowerShot A470', 0x2430000: 'PowerShot SD1100 IS / Digital IXUS 80 IS / IXY Digital 20 IS', 0x2460000: 'PowerShot SX1 IS', 0x2470000: 'PowerShot SX10 IS', 0x2480000: 'PowerShot A1000 IS', 0x2490000: 'PowerShot G10', 0x2510000: 'PowerShot A2000 IS', 0x2520000: 'PowerShot SX110 IS', 0x2530000: 'PowerShot SD990 IS / Digital IXUS 980 IS / IXY Digital 3000 IS', 0x2540000: 'PowerShot SD880 IS / Digital IXUS 870 IS / IXY Digital 920 IS', 0x2550000: 'PowerShot E1', 0x2560000: 'PowerShot D10', 0x2570000: 'PowerShot SD960 IS / Digital IXUS 110 IS / IXY Digital 510 IS', 0x2580000: 'PowerShot A2100 IS', 0x2590000: 'PowerShot A480', 0x2600000: 'PowerShot SX200 IS', 0x2610000: 'PowerShot SD970 IS / Digital IXUS 990 IS / IXY Digital 830 IS', 0x2620000: 'PowerShot SD780 IS / Digital IXUS 100 IS / IXY Digital 210 IS', 0x2630000: 'PowerShot A1100 IS', 0x2640000: 'PowerShot SD1200 IS / Digital IXUS 95 IS / IXY Digital 110 IS', 0x2700000: 'PowerShot G11', 0x2710000: 'PowerShot SX120 IS', 0x2720000: 'PowerShot S90', 0x2750000: 'PowerShot SX20 IS', 0x2760000: 'PowerShot SD980 IS / Digital IXUS 200 IS / IXY Digital 930 IS', 0x2770000: 'PowerShot SD940 IS / Digital IXUS 120 IS / IXY Digital 220 IS', 0x2800000: 'PowerShot A495', 0x2810000: 'PowerShot A490', 0x2820000: 'PowerShot A3100 IS / A3150 IS', 0x2830000: 'PowerShot A3000 IS', 0x2840000: 'PowerShot SD1400 IS / IXUS 130 / IXY 400F', 0x2850000: 'PowerShot SD1300 IS / IXUS 105 / IXY 200F', 0x2860000: 'PowerShot SD3500 IS / IXUS 210 / IXY 10S', 0x2870000: 'PowerShot SX210 IS', 0x2880000: 'PowerShot SD4000 IS / IXUS 300 HS / IXY 30S', 0x2890000: 'PowerShot SD4500 IS / IXUS 1000 HS / IXY 50S', 0x2920000: 'PowerShot G12', 0x2930000: 'PowerShot SX30 IS', 0x2940000: 'PowerShot SX130 IS', 0x2950000: 'PowerShot S95', 0x2980000: 'PowerShot A3300 IS', 0x2990000: 'PowerShot A3200 IS', 0x3000000: 'PowerShot ELPH 500 HS / IXUS 310 HS / IXY 31S', 0x3010000: 'PowerShot Pro90 IS', 0x3010001: 'PowerShot A800', 0x3020000: 'PowerShot ELPH 100 HS / IXUS 115 HS / IXY 210F', 0x3030000: 'PowerShot SX230 HS', 0x3040000: 'PowerShot ELPH 300 HS / IXUS 220 HS / IXY 410F', 0x3050000: 'PowerShot A2200', 0x3060000: 'PowerShot A1200', 0x3070000: 'PowerShot SX220 HS', 0x3080000: 'PowerShot G1 X', 0x3090000: 'PowerShot SX150 IS', 0x3100000: 'PowerShot ELPH 510 HS / IXUS 1100 HS / IXY 51S', 0x3110000: 'PowerShot S100 (new)', 0x3130000: 'PowerShot SX40 HS', 0x3120000: 'PowerShot ELPH 310 HS / IXUS 230 HS / IXY 600F', 0x3160000: 'PowerShot A1300', 0x3170000: 'PowerShot A810', 0x3180000: 'PowerShot ELPH 320 HS / IXUS 240 HS / IXY 420F', 0x3190000: 'PowerShot ELPH 110 HS / IXUS 125 HS / IXY 220F', 0x3200000: 'PowerShot D20', 0x3210000: 'PowerShot A4000 IS', 0x3220000: 'PowerShot SX260 HS', 0x3230000: 'PowerShot SX240 HS', 0x3240000: 'PowerShot ELPH 530 HS / IXUS 510 HS / IXY 1', 0x3250000: 'PowerShot ELPH 520 HS / IXUS 500 HS / IXY 3', 0x3260000: 'PowerShot A3400 IS', 0x3270000: 'PowerShot A2400 IS', 0x3280000: 'PowerShot A2300', 0x3330000: 'PowerShot G15', 0x3340000: 'PowerShot SX50', 0x3350000: 'PowerShot SX160 IS', 0x3360000: 'PowerShot S110 (new)', 0x3370000: 'PowerShot SX500 IS', 0x3380000: 'PowerShot N', 0x3390000: 'IXUS 245 HS / IXY 430F', 0x3400000: 'PowerShot SX280 HS', 0x3410000: 'PowerShot SX270 HS', 0x3420000: 'PowerShot A3500 IS', 0x3430000: 'PowerShot A2600', 0x3450000: 'PowerShot A1400', 0x3460000: 'PowerShot ELPH 130 IS / IXUS 140 / IXY 110F', 0x3470000: 'PowerShot ELPH 115/120 IS / IXUS 132/135 / IXY 90F/100F', 0x3490000: 'PowerShot ELPH 330 HS / IXUS 255 HS / IXY 610F', 0x3510000: 'PowerShot A2500', 0x3540000: 'PowerShot G16', 0x3550000: 'PowerShot S120', 0x3560000: 'PowerShot SX170 IS', 0x3580000: 'PowerShot SX510 HS', 0x3590000: 'PowerShot S200 (new)', 0x3600000: 'IXY 620F', 0x3610000: 'PowerShot N100', 0x3640000: 'PowerShot G1 X Mark II', 0x3650000: 'PowerShot D30', 0x3660000: 'PowerShot SX700 HS', 0x3670000: 'PowerShot SX600 HS', 0x3680000: 'PowerShot ELPH 140 IS / IXUS 150 / IXY 130', 0x3690000: 'PowerShot ELPH 135 / IXUS 145 / IXY 120', 0x3700000: 'PowerShot ELPH 340 HS / IXUS 265 HS / IXY 630', 0x3710000: 'PowerShot ELPH 150 IS / IXUS 155 / IXY 140', 0x3740000: 'EOS M3', 0x3750000: 'PowerShot SX60 HS', 0x3760000: 'PowerShot SX520 HS', 0x3770000: 'PowerShot SX400 IS', 0x3780000: 'PowerShot G7 X', 0x3790000: 'PowerShot N2', 0x3800000: 'PowerShot SX530 HS', 0x3820000: 'PowerShot SX710 HS', 0x3830000: 'PowerShot SX610 HS', 0x3870000: 'PowerShot ELPH 160 / IXUS 160', 0x3890000: 'PowerShot ELPH 170 IS / IXUS 170', 0x3910000: 'PowerShot SX410 IS', 0x4040000: 'PowerShot G1', 0x6040000: 'PowerShot S100 / Digital IXUS / IXY Digital', 0x4007d673: 'DC19/DC21/DC22', 0x4007d674: 'XH A1', 0x4007d675: 'HV10', 0x4007d676: 'MD130/MD140/MD150/MD160/ZR850', 0x4007d777: 'DC50', 0x4007d778: 'HV20', 0x4007d779: 'DC211', 0x4007d77a: 'HG10', 0x4007d77b: 'HR10', 0x4007d77d: 'MD255/ZR950', 0x4007d81c: 'HF11', 0x4007d878: 'HV30', 0x4007d87c: 'XH A1S', 0x4007d87e: 'DC301/DC310/DC311/DC320/DC330', 0x4007d87f: 'FS100', 0x4007d880: 'HF10', 0x4007d882: 'HG20/HG21', 0x4007d925: 'HF21', 0x4007d926: 'HF S11', 0x4007d978: 'HV40', 0x4007d987: 'DC410/DC411/DC420', 0x4007d988: 'FS19/FS20/FS21/FS22/FS200', 0x4007d989: 'HF20/HF200', 0x4007d98a: 'HF S10/S100', 0x4007da8e: 'HF R10/R16/R17/R18/R100/R106', 0x4007da8f: 'HF M30/M31/M36/M300/M306', 0x4007da90: 'HF S20/S21/S200', 0x4007da92: 'FS31/FS36/FS37/FS300/FS305/FS306/FS307', 0x4007dda9: 'HF G25', 0x80000001: 'EOS-1D', 0x80000167: 'EOS-1DS', 0x80000168: 'EOS 10D', 0x80000169: 'EOS-1D Mark III', 0x80000170: 'EOS Digital Rebel / 300D / Kiss Digital', 0x80000174: 'EOS-1D Mark II', 0x80000175: 'EOS 20D', 0x80000176: 'EOS Digital Rebel XSi / 450D / Kiss X2', 0x80000188: 'EOS-1Ds Mark II', 0x80000189: 'EOS Digital Rebel XT / 350D / Kiss Digital N', 0x80000190: 'EOS 40D', 0x80000213: 'EOS 5D', 0x80000215: 'EOS-1Ds Mark III', 0x80000218: 'EOS 5D Mark II', 0x80000219: 'WFT-E1', 0x80000232: 'EOS-1D Mark II N', 0x80000234: 'EOS 30D', 0x80000236: 'EOS Digital Rebel XTi / 400D / Kiss Digital X', 0x80000241: 'WFT-E2', 0x80000246: 'WFT-E3', 0x80000250: 'EOS 7D', 0x80000252: 'EOS Rebel T1i / 500D / Kiss X3', 0x80000254: 'EOS Rebel XS / 1000D / Kiss F', 0x80000261: 'EOS 50D', 0x80000269: 'EOS-1D X', 0x80000270: 'EOS Rebel T2i / 550D / Kiss X4', 0x80000271: 'WFT-E4', 0x80000273: 'WFT-E5', 0x80000281: 'EOS-1D Mark IV', 0x80000285: 'EOS 5D Mark III', 0x80000286: 'EOS Rebel T3i / 600D / Kiss X5', 0x80000287: 'EOS 60D', 0x80000288: 'EOS Rebel T3 / 1100D / Kiss X50', 0x80000289: 'EOS 7D Mark II', 0x80000297: 'WFT-E2 II', 0x80000298: 'WFT-E4 II', 0x80000301: 'EOS Rebel T4i / 650D / Kiss X6i', 0x80000302: 'EOS 6D', 0x80000324: 'EOS-1D C', 0x80000325: 'EOS 70D', 0x80000326: 'EOS Rebel T5i / 700D / Kiss X7i', 0x80000327: 'EOS Rebel T5 / 1200D / Kiss X70', 0x80000331: 'EOS M', 0x80000355: 'EOS M2', 0x80000346: 'EOS Rebel SL1 / 100D / Kiss X7', 0x80000347: 'EOS Rebel T6s / 760D / 8000D', 0x80000382: 'EOS 5DS', 0x80000393: 'EOS Rebel T6i / 750D / Kiss X8i', 0x80000401: 'EOS 5DS R', }), 0x0013: ('ThumbnailImageValidArea', ), 0x0015: ('SerialNumberFormat', { 0x90000000: 'Format 1', 0xA0000000: 'Format 2' }), 0x001a: ('SuperMacro', { 0: 'Off', 1: 'On (1)', 2: 'On (2)' }), 0x001c: ('DateStampMode', { 0: 'Off', 1: 'Date', 2: 'Date & Time', }), 0x001e: ('FirmwareRevision', ), 0x0028: ('ImageUniqueID', ), 0x0095: ('LensModel', ), 0x0096: ('InternalSerialNumber ', ), 0x0097: ('DustRemovalData ', ), 0x0098: ('CropInfo ', ), 0x009a: ('AspectInfo', ), 0x00b4: ('ColorSpace', { 1: 'sRGB', 2: 'Adobe RGB' }), } # this is in element offset, name, optional value dictionary format # 0x0001 CAMERA_SETTINGS = { 1: ('Macromode', { 1: 'Macro', 2: 'Normal' }), 2: ('SelfTimer', ), 3: ('Quality', { 1: 'Economy', 2: 'Normal', 3: 'Fine', 5: 'Superfine' }), 4: ('FlashMode', { 0: 'Flash Not Fired', 1: 'Auto', 2: 'On', 3: 'Red-Eye Reduction', 4: 'Slow Synchro', 5: 'Auto + Red-Eye Reduction', 6: 'On + Red-Eye Reduction', 16: 'external flash' }), 5: ('ContinuousDriveMode', { 0: 'Single Or Timer', 1: 'Continuous', 2: 'Movie', }), 7: ('FocusMode', { 0: 'One-Shot', 1: 'AI Servo', 2: 'AI Focus', 3: 'MF', 4: 'Single', 5: 'Continuous', 6: 'MF' }), 9: ('RecordMode', { 1: 'JPEG', 2: 'CRW+THM', 3: 'AVI+THM', 4: 'TIF', 5: 'TIF+JPEG', 6: 'CR2', 7: 'CR2+JPEG', 9: 'Video' }), 10: ('ImageSize', { 0: 'Large', 1: 'Medium', 2: 'Small' }), 11: ('EasyShootingMode', { 0: 'Full Auto', 1: 'Manual', 2: 'Landscape', 3: 'Fast Shutter', 4: 'Slow Shutter', 5: 'Night', 6: 'B&W', 7: 'Sepia', 8: 'Portrait', 9: 'Sports', 10: 'Macro/Close-Up', 11: 'Pan Focus', 51: 'High Dynamic Range', }), 12: ('DigitalZoom', { 0: 'None', 1: '2x', 2: '4x', 3: 'Other' }), 13: ('Contrast', { 0xFFFF: 'Low', 0: 'Normal', 1: 'High' }), 14: ('Saturation', { 0xFFFF: 'Low', 0: 'Normal', 1: 'High' }), 15: ('Sharpness', { 0xFFFF: 'Low', 0: 'Normal', 1: 'High' }), 16: ('ISO', { 0: 'See ISOSpeedRatings Tag', 15: 'Auto', 16: '50', 17: '100', 18: '200', 19: '400' }), 17: ('MeteringMode', { 0: 'Default', 1: 'Spot', 2: 'Average', 3: 'Evaluative', 4: 'Partial', 5: 'Center-weighted' }), 18: ('FocusType', { 0: 'Manual', 1: 'Auto', 3: 'Close-Up (Macro)', 8: 'Locked (Pan Mode)' }), 19: ('AFPointSelected', { 0x3000: 'None (MF)', 0x3001: 'Auto-Selected', 0x3002: 'Right', 0x3003: 'Center', 0x3004: 'Left' }), 20: ('ExposureMode', { 0: 'Easy Shooting', 1: 'Program', 2: 'Tv-priority', 3: 'Av-priority', 4: 'Manual', 5: 'A-DEP' }), 22: ('LensType', ), 23: ('LongFocalLengthOfLensInFocalUnits', ), 24: ('ShortFocalLengthOfLensInFocalUnits', ), 25: ('FocalUnitsPerMM', ), 28: ('FlashActivity', { 0: 'Did Not Fire', 1: 'Fired' }), 29: ('FlashDetails', { 0: 'Manual', 1: 'TTL', 2: 'A-TTL', 3: 'E-TTL', 4: 'FP Sync Enabled', 7: '2nd("Rear")-Curtain Sync Used', 11: 'FP Sync Used', 13: 'Internal Flash', 14: 'External E-TTL' }), 32: ('FocusMode', { 0: 'Single', 1: 'Continuous', 8: 'Manual' }), 33: ('AESetting', { 0: 'Normal AE', 1: 'Exposure Compensation', 2: 'AE Lock', 3: 'AE Lock + Exposure Comp.', 4: 'No AE' }), 34: ('ImageStabilization', { 0: 'Off', 1: 'On', 2: 'Shoot Only', 3: 'Panning', 4: 'Dynamic', 256: 'Off', 257: 'On', 258: 'Shoot Only', 259: 'Panning', 260: 'Dynamic' }), 39: ('SpotMeteringMode', { 0: 'Center', 1: 'AF Point' }), 41: ('ManualFlashOutput', { 0x0: 'n/a', 0x500: 'Full', 0x502: 'Medium', 0x504: 'Low', 0x7fff: 'n/a' }), } # 0x0002 FOCAL_LENGTH = { 1: ('FocalType', { 1: 'Fixed', 2: 'Zoom', }), 2: ('FocalLength', ), } # 0x0004 SHOT_INFO = { 7: ('WhiteBalance', { 0: 'Auto', 1: 'Sunny', 2: 'Cloudy', 3: 'Tungsten', 4: 'Fluorescent', 5: 'Flash', 6: 'Custom' }), 8: ('SlowShutter', { -1: 'n/a', 0: 'Off', 1: 'Night Scene', 2: 'On', 3: 'None' }), 9: ('SequenceNumber', ), 14: ('AFPointUsed', ), 15: ('FlashBias', { 0xFFC0: '-2 EV', 0xFFCC: '-1.67 EV', 0xFFD0: '-1.50 EV', 0xFFD4: '-1.33 EV', 0xFFE0: '-1 EV', 0xFFEC: '-0.67 EV', 0xFFF0: '-0.50 EV', 0xFFF4: '-0.33 EV', 0x0000: '0 EV', 0x000c: '0.33 EV', 0x0010: '0.50 EV', 0x0014: '0.67 EV', 0x0020: '1 EV', 0x002c: '1.33 EV', 0x0030: '1.50 EV', 0x0034: '1.67 EV', 0x0040: '2 EV' }), 19: ('SubjectDistance', ), } # 0x0026 AF_INFO_2 = { 2: ('AFAreaMode', { 0: 'Off (Manual Focus)', 2: 'Single-point AF', 4: 'Multi-point AF or AI AF', 5: 'Face Detect AF', 6: 'Face + Tracking', 7: 'Zone AF', 8: 'AF Point Expansion', 9: 'Spot AF', 11: 'Flexizone Multi', 13: 'Flexizone Single', }), 3: ('NumAFPoints', ), 4: ('ValidAFPoints', ), 5: ('CanonImageWidth', ), } # 0x0093 FILE_INFO = { 1: ('FileNumber', ), 3: ('BracketMode', { 0: 'Off', 1: 'AEB', 2: 'FEB', 3: 'ISO', 4: 'WB', }), 4: ('BracketValue', ), 5: ('BracketShotNumber', ), 6: ('RawJpgQuality', { 0xFFFF: 'n/a', 1: 'Economy', 2: 'Normal', 3: 'Fine', 4: 'RAW', 5: 'Superfine', 130: 'Normal Movie' }), 7: ('RawJpgSize', { 0: 'Large', 1: 'Medium', 2: 'Small', 5: 'Medium 1', 6: 'Medium 2', 7: 'Medium 3', 8: 'Postcard', 9: 'Widescreen', 10: 'Medium Widescreen', 14: 'Small 1', 15: 'Small 2', 16: 'Small 3', 128: '640x480 Movie', 129: 'Medium Movie', 130: 'Small Movie', 137: '1280x720 Movie', 142: '1920x1080 Movie', }), 8: ('LongExposureNoiseReduction2', { 0: 'Off', 1: 'On (1D)', 2: 'On', 3: 'Auto' }), 9: ('WBBracketMode', { 0: 'Off', 1: 'On (shift AB)', 2: 'On (shift GM)' }), 12: ('WBBracketValueAB', ), 13: ('WBBracketValueGM', ), 14: ('FilterEffect', { 0: 'None', 1: 'Yellow', 2: 'Orange', 3: 'Red', 4: 'Green' }), 15: ('ToningEffect', { 0: 'None', 1: 'Sepia', 2: 'Blue', 3: 'Purple', 4: 'Green', }), 16: ('MacroMagnification', ), 19: ('LiveViewShooting', { 0: 'Off', 1: 'On' }), 25: ('FlashExposureLock', { 0: 'Off', 1: 'On' }) } def add_one(value): return value + 1 def subtract_one(value): return value - 1 def convert_temp(value): return '%d C' % (value - 128) # CameraInfo data structures have variable sized members. Each entry here is: # byte offset: (item name, data item type, decoding map). # Note that the data item type is fed directly to struct.unpack at the # specified offset. CAMERA_INFO_TAG_NAME = 'MakerNote Tag 0x000D' CAMERA_INFO_5D = { 23: ('CameraTemperature', ' str: """ First digit seems to be in steps of 1/6 EV. Does the third value mean the step size? It is usually 6, but it is 12 for the ExposureDifference. Check for an error condition that could cause a crash. This only happens if something has gone really wrong in reading the Nikon MakerNote. http://tomtia.plala.jp/DigitalCamera/MakerNote/index.asp """ if len(seq) < 4: return '' if seq == [252, 1, 6, 0]: return '-2/3 EV' if seq == [253, 1, 6, 0]: return '-1/2 EV' if seq == [254, 1, 6, 0]: return '-1/3 EV' if seq == [0, 1, 6, 0]: return '0 EV' if seq == [2, 1, 6, 0]: return '+1/3 EV' if seq == [3, 1, 6, 0]: return '+1/2 EV' if seq == [4, 1, 6, 0]: return '+2/3 EV' # Handle combinations not in the table. i = seq[0] # Causes headaches for the +/- logic, so special case it. if i == 0: return '0 EV' if i > 127: i = 256 - i ret_str = '-' else: ret_str = '+' step = seq[2] # Assume third value means the step size whole = i / step i = i % step if whole != 0: ret_str = '%s%s ' % (ret_str, str(whole)) if i == 0: ret_str += 'EV' else: ratio = Ratio(i, step) ret_str = ret_str + str(ratio) + ' EV' return ret_str # Nikon E99x MakerNote Tags TAGS_NEW = { 0x0001: ('MakernoteVersion', make_string), # Sometimes binary 0x0002: ('ISOSetting', ), 0x0003: ('ColorMode', ), 0x0004: ('Quality', ), 0x0005: ('Whitebalance', ), 0x0006: ('ImageSharpening', ), 0x0007: ('FocusMode', ), 0x0008: ('FlashSetting', ), 0x0009: ('AutoFlashMode', ), 0x000B: ('WhiteBalanceBias', ), 0x000C: ('WhiteBalanceRBCoeff', ), 0x000D: ('ProgramShift', ev_bias), # Nearly the same as the other EV vals, but step size is 1/12 EV (?) 0x000E: ('ExposureDifference', ev_bias), 0x000F: ('ISOSelection', ), 0x0010: ('DataDump', ), 0x0011: ('NikonPreview', ), 0x0012: ('FlashCompensation', ev_bias), 0x0013: ('ISOSpeedRequested', ), 0x0016: ('PhotoCornerCoordinates', ), 0x0017: ('ExternalFlashExposureComp', ev_bias), 0x0018: ('FlashBracketCompensationApplied', ev_bias), 0x0019: ('AEBracketCompensationApplied', ), 0x001A: ('ImageProcessing', ), 0x001B: ('CropHiSpeed', ), 0x001C: ('ExposureTuning', ), 0x001D: ('SerialNumber', ), # Conflict with 0x00A0 ? 0x001E: ('ColorSpace', ), 0x001F: ('VRInfo', ), 0x0020: ('ImageAuthentication', ), 0x0022: ('ActiveDLighting', ), 0x0023: ('PictureControl', ), 0x0024: ('WorldTime', ), 0x0025: ('ISOInfo', ), 0x0080: ('ImageAdjustment', ), 0x0081: ('ToneCompensation', ), 0x0082: ('AuxiliaryLens', ), 0x0083: ('LensType', ), 0x0084: ('LensMinMaxFocalMaxAperture', ), 0x0085: ('ManualFocusDistance', ), 0x0086: ('DigitalZoomFactor', ), 0x0087: ('FlashMode', { 0x00: 'Did Not Fire', 0x01: 'Fired, Manual', 0x07: 'Fired, External', 0x08: 'Fired, Commander Mode ', 0x09: 'Fired, TTL Mode', }), 0x0088: ('AFFocusPosition', { 0x0000: 'Center', 0x0100: 'Top', 0x0200: 'Bottom', 0x0300: 'Left', 0x0400: 'Right', }), 0x0089: ('BracketingMode', { 0x00: 'Single frame, no bracketing', 0x01: 'Continuous, no bracketing', 0x02: 'Timer, no bracketing', 0x10: 'Single frame, exposure bracketing', 0x11: 'Continuous, exposure bracketing', 0x12: 'Timer, exposure bracketing', 0x40: 'Single frame, white balance bracketing', 0x41: 'Continuous, white balance bracketing', 0x42: 'Timer, white balance bracketing' }), 0x008A: ('AutoBracketRelease', ), 0x008B: ('LensFStops', ), 0x008C: ('NEFCurve1', ), # ExifTool calls this 'ContrastCurve' 0x008D: ('ColorMode', ), 0x008F: ('SceneMode', ), 0x0090: ('LightingType', ), 0x0091: ('ShotInfo', ), # First 4 bytes are a version number in ASCII 0x0092: ('HueAdjustment', ), # ExifTool calls this 'NEFCompression', should be 1-4 0x0093: ('Compression', ), 0x0094: ('Saturation', { -3: 'B&W', -2: '-2', -1: '-1', 0: '0', 1: '1', 2: '2', }), 0x0095: ('NoiseReduction', ), 0x0096: ('NEFCurve2', ), # ExifTool calls this 'LinearizationTable' 0x0097: ('ColorBalance', ), # First 4 bytes are a version number in ASCII 0x0098: ('LensData', ), # First 4 bytes are a version number in ASCII 0x0099: ('RawImageCenter', ), 0x009A: ('SensorPixelSize', ), 0x009C: ('Scene Assist', ), 0x009E: ('RetouchHistory', ), 0x00A0: ('SerialNumber', ), 0x00A2: ('ImageDataSize', ), # 00A3: unknown - a single byte 0 # 00A4: In NEF, looks like a 4 byte ASCII version number ('0200') 0x00A5: ('ImageCount', ), 0x00A6: ('DeletedImageCount', ), 0x00A7: ('TotalShutterReleases', ), # First 4 bytes are a version number in ASCII, with version specific # info to follow. Its hard to treat it as a string due to embedded nulls. 0x00A8: ('FlashInfo', ), 0x00A9: ('ImageOptimization', ), 0x00AA: ('Saturation', ), 0x00AB: ('DigitalVariProgram', ), 0x00AC: ('ImageStabilization', ), 0x00AD: ('AFResponse', ), 0x00B0: ('MultiExposure', ), 0x00B1: ('HighISONoiseReduction', ), 0x00B6: ('PowerUpTime', ), 0x00B7: ('AFInfo2', ), 0x00B8: ('FileInfo', ), 0x00B9: ('AFTune', ), 0x0100: ('DigitalICE', ), 0x0103: ('PreviewCompression', { 1: 'Uncompressed', 2: 'CCITT 1D', 3: 'T4/Group 3 Fax', 4: 'T6/Group 4 Fax', 5: 'LZW', 6: 'JPEG (old-style)', 7: 'JPEG', 8: 'Adobe Deflate', 9: 'JBIG B&W', 10: 'JBIG Color', 32766: 'Next', 32769: 'Epson ERF Compressed', 32771: 'CCIRLEW', 32773: 'PackBits', 32809: 'Thunderscan', 32895: 'IT8CTPAD', 32896: 'IT8LW', 32897: 'IT8MP', 32898: 'IT8BL', 32908: 'PixarFilm', 32909: 'PixarLog', 32946: 'Deflate', 32947: 'DCS', 34661: 'JBIG', 34676: 'SGILog', 34677: 'SGILog24', 34712: 'JPEG 2000', 34713: 'Nikon NEF Compressed', 65000: 'Kodak DCR Compressed', 65535: 'Pentax PEF Compressed', }), 0x0201: ('PreviewImageStart', ), 0x0202: ('PreviewImageLength', ), 0x0213: ('PreviewYCbCrPositioning', { 1: 'Centered', 2: 'Co-sited', }), 0x0E09: ('NikonCaptureVersion', ), 0x0E0E: ('NikonCaptureOffsets', ), 0x0E10: ('NikonScan', ), 0x0E22: ('NEFBitDepth', ), } TAGS_OLD = { 0x0003: ('Quality', { 1: 'VGA Basic', 2: 'VGA Normal', 3: 'VGA Fine', 4: 'SXGA Basic', 5: 'SXGA Normal', 6: 'SXGA Fine', }), 0x0004: ('ColorMode', { 1: 'Color', 2: 'Monochrome', }), 0x0005: ('ImageAdjustment', { 0: 'Normal', 1: 'Bright+', 2: 'Bright-', 3: 'Contrast+', 4: 'Contrast-', }), 0x0006: ('CCDSpeed', { 0: 'ISO 80', 2: 'ISO 160', 4: 'ISO 320', 5: 'ISO 100', }), 0x0007: ('WhiteBalance', { 0: 'Auto', 1: 'Preset', 2: 'Daylight', 3: 'Incandescent', 4: 'Fluorescent', 5: 'Cloudy', 6: 'Speed Light', }), } exif-py-3.0.0/exifread/tags/makernote/olympus.py000066400000000000000000000172411423577444100217070ustar00rootroot00000000000000 from exifread.utils import make_string def special_mode(val): """Decode Olympus SpecialMode tag in MakerNote""" mode1 = { 0: 'Normal', 1: 'Unknown', 2: 'Fast', 3: 'Panorama', } mode2 = { 0: 'Non-panoramic', 1: 'Left to right', 2: 'Right to left', 3: 'Bottom to top', 4: 'Top to bottom', } if not val: return val mode1_val = mode1.get(val[0], "Unknown") mode2_val = mode2.get(val[2], "Unknown") return '%s - Sequence %d - %s' % (mode1_val, val[1], mode2_val) TAGS = { # Ah HAH! those sneeeeeaky bastids! this is how they get past the fact # that a JPEG thumbnail is not allowed in an uncompressed TIFF file 0x0100: ('JPEGThumbnail', ), 0x0200: ('SpecialMode', special_mode), 0x0201: ('JPEGQual', { 1: 'SQ', 2: 'HQ', 3: 'SHQ', }), 0x0202: ('Macro', { 0: 'Normal', 1: 'Macro', 2: 'SuperMacro' }), 0x0203: ('BWMode', { 0: 'Off', 1: 'On' }), 0x0204: ('DigitalZoom', ), 0x0205: ('FocalPlaneDiagonal', ), 0x0206: ('LensDistortionParams', ), 0x0207: ('SoftwareRelease', ), 0x0208: ('PictureInfo', ), 0x0209: ('CameraID', make_string), # print as string 0x0F00: ('DataDump', ), 0x0300: ('PreCaptureFrames', ), 0x0404: ('SerialNumber', ), 0x1000: ('ShutterSpeedValue', ), 0x1001: ('ISOValue', ), 0x1002: ('ApertureValue', ), 0x1003: ('BrightnessValue', ), 0x1004: ('FlashMode', { 2: 'On', 3: 'Off' }), 0x1005: ('FlashDevice', { 0: 'None', 1: 'Internal', 4: 'External', 5: 'Internal + External' }), 0x1006: ('ExposureCompensation', ), 0x1007: ('SensorTemperature', ), 0x1008: ('LensTemperature', ), 0x100b: ('FocusMode', { 0: 'Auto', 1: 'Manual' }), 0x1017: ('RedBalance', ), 0x1018: ('BlueBalance', ), 0x101a: ('SerialNumber', ), 0x1023: ('FlashExposureComp', ), 0x1026: ('ExternalFlashBounce', { 0: 'No', 1: 'Yes' }), 0x1027: ('ExternalFlashZoom', ), 0x1028: ('ExternalFlashMode', ), 0x1029: ('Contrast int16u', { 0: 'High', 1: 'Normal', 2: 'Low' }), 0x102a: ('SharpnessFactor', ), 0x102b: ('ColorControl', ), 0x102c: ('ValidBits', ), 0x102d: ('CoringFilter', ), 0x102e: ('OlympusImageWidth', ), 0x102f: ('OlympusImageHeight', ), 0x1034: ('CompressionRatio', ), 0x1035: ('PreviewImageValid', { 0: 'No', 1: 'Yes' }), 0x1036: ('PreviewImageStart', ), 0x1037: ('PreviewImageLength', ), 0x1039: ('CCDScanMode', { 0: 'Interlaced', 1: 'Progressive' }), 0x103a: ('NoiseReduction', { 0: 'Off', 1: 'On' }), 0x103b: ('InfinityLensStep', ), 0x103c: ('NearLensStep', ), # TODO - these need extra definitions # http://search.cpan.org/src/EXIFTOOL/Image-ExifTool-6.90/html/TagNames/Olympus.html 0x2010: ('Equipment', ), 0x2020: ('CameraSettings', ), 0x2030: ('RawDevelopment', ), 0x2040: ('ImageProcessing', ), 0x2050: ('FocusInfo', ), 0x3000: ('RawInfo ', ), } # 0x2020 CameraSettings TAG_0x2020 = { 0x0100: ('PreviewImageValid', { 0: 'No', 1: 'Yes' }), 0x0101: ('PreviewImageStart', ), 0x0102: ('PreviewImageLength', ), 0x0200: ('ExposureMode', { 1: 'Manual', 2: 'Program', 3: 'Aperture-priority AE', 4: 'Shutter speed priority AE', 5: 'Program-shift' }), 0x0201: ('AELock', { 0: 'Off', 1: 'On' }), 0x0202: ('MeteringMode', { 2: 'Center Weighted', 3: 'Spot', 5: 'ESP', 261: 'Pattern+AF', 515: 'Spot+Highlight control', 1027: 'Spot+Shadow control' }), 0x0300: ('MacroMode', { 0: 'Off', 1: 'On' }), 0x0301: ('FocusMode', { 0: 'Single AF', 1: 'Sequential shooting AF', 2: 'Continuous AF', 3: 'Multi AF', 10: 'MF' }), 0x0302: ('FocusProcess', { 0: 'AF Not Used', 1: 'AF Used' }), 0x0303: ('AFSearch', { 0: 'Not Ready', 1: 'Ready' }), 0x0304: ('AFAreas', ), 0x0401: ('FlashExposureCompensation', ), 0x0500: ('WhiteBalance2', { 0: 'Auto', 16: '7500K (Fine Weather with Shade)', 17: '6000K (Cloudy)', 18: '5300K (Fine Weather)', 20: '3000K (Tungsten light)', 21: '3600K (Tungsten light-like)', 33: '6600K (Daylight fluorescent)', 34: '4500K (Neutral white fluorescent)', 35: '4000K (Cool white fluorescent)', 48: '3600K (Tungsten light-like)', 256: 'Custom WB 1', 257: 'Custom WB 2', 258: 'Custom WB 3', 259: 'Custom WB 4', 512: 'Custom WB 5400K', 513: 'Custom WB 2900K', 514: 'Custom WB 8000K', }), 0x0501: ('WhiteBalanceTemperature', ), 0x0502: ('WhiteBalanceBracket', ), 0x0503: ('CustomSaturation', ), # (3 numbers: 1. CS Value, 2. Min, 3. Max) 0x0504: ('ModifiedSaturation', { 0: 'Off', 1: 'CM1 (Red Enhance)', 2: 'CM2 (Green Enhance)', 3: 'CM3 (Blue Enhance)', 4: 'CM4 (Skin Tones)', }), 0x0505: ('ContrastSetting', ), # (3 numbers: 1. Contrast, 2. Min, 3. Max) 0x0506: ('SharpnessSetting', ), # (3 numbers: 1. Sharpness, 2. Min, 3. Max) 0x0507: ('ColorSpace', { 0: 'sRGB', 1: 'Adobe RGB', 2: 'Pro Photo RGB' }), 0x0509: ('SceneMode', { 0: 'Standard', 6: 'Auto', 7: 'Sport', 8: 'Portrait', 9: 'Landscape+Portrait', 10: 'Landscape', 11: 'Night scene', 13: 'Panorama', 16: 'Landscape+Portrait', 17: 'Night+Portrait', 19: 'Fireworks', 20: 'Sunset', 22: 'Macro', 25: 'Documents', 26: 'Museum', 28: 'Beach&Snow', 30: 'Candle', 35: 'Underwater Wide1', 36: 'Underwater Macro', 39: 'High Key', 40: 'Digital Image Stabilization', 44: 'Underwater Wide2', 45: 'Low Key', 46: 'Children', 48: 'Nature Macro', }), 0x050a: ('NoiseReduction', { 0: 'Off', 1: 'Noise Reduction', 2: 'Noise Filter', 3: 'Noise Reduction + Noise Filter', 4: 'Noise Filter (ISO Boost)', 5: 'Noise Reduction + Noise Filter (ISO Boost)' }), 0x050b: ('DistortionCorrection', { 0: 'Off', 1: 'On' }), 0x050c: ('ShadingCompensation', { 0: 'Off', 1: 'On' }), 0x050d: ('CompressionFactor', ), 0x050f: ('Gradation', { '-1 -1 1': 'Low Key', '0 -1 1': 'Normal', '1 -1 1': 'High Key' }), 0x0520: ('PictureMode', { 1: 'Vivid', 2: 'Natural', 3: 'Muted', 256: 'Monotone', 512: 'Sepia' }), 0x0521: ('PictureModeSaturation', ), 0x0522: ('PictureModeHue?', ), 0x0523: ('PictureModeContrast', ), 0x0524: ('PictureModeSharpness', ), 0x0525: ('PictureModeBWFilter', { 0: 'n/a', 1: 'Neutral', 2: 'Yellow', 3: 'Orange', 4: 'Red', 5: 'Green' }), 0x0526: ('PictureModeTone', { 0: 'n/a', 1: 'Neutral', 2: 'Sepia', 3: 'Blue', 4: 'Purple', 5: 'Green' }), 0x0600: ('Sequence', ), # 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits 0x0601: ('PanoramaMode', ), # (2 numbers: 1. Mode, 2. Shot number) 0x0603: ('ImageQuality2', { 1: 'SQ', 2: 'HQ', 3: 'SHQ', 4: 'RAW', }), 0x0901: ('ManometerReading', ), } exif-py-3.0.0/exifread/utils.py000066400000000000000000000060561423577444100164160ustar00rootroot00000000000000""" Misc utilities. """ from fractions import Fraction from typing import Union def ord_(dta): if isinstance(dta, str): return ord(dta) return dta def make_string(seq: Union[bytes, list]) -> str: """ Don't throw an exception when given an out of range character. """ string = '' for char in seq: # Screen out non-printing characters try: if 32 <= char < 256: string += chr(char) except TypeError: pass # If no printing chars if not string: if isinstance(seq, list): string = ''.join(map(str, seq)) # Some UserComment lists only contain null bytes, nothing valuable to return if set(string) == {'0'}: return '' else: string = str(seq) # Clean undesirable characters on any end return string.strip(' \x00') def make_string_uc(seq) -> str: """ Special version to deal with the code in the first 8 bytes of a user comment. First 8 bytes gives coding system e.g. ASCII vs. JIS vs Unicode. """ if not isinstance(seq, str): # Remove code from sequence only if it is valid if make_string(seq[:8]).upper() in ('ASCII', 'UNICODE', 'JIS', ''): seq = seq[8:] # Of course, this is only correct if ASCII, and the standard explicitly # allows JIS and Unicode. return make_string(seq) def get_gps_coords(tags: dict) -> tuple: lng_ref_tag_name = 'GPS GPSLongitudeRef' lng_tag_name = 'GPS GPSLongitude' lat_ref_tag_name = 'GPS GPSLatitudeRef' lat_tag_name = 'GPS GPSLatitude' # Check if these tags are present gps_tags = [lng_ref_tag_name, lng_tag_name, lat_tag_name, lat_tag_name] for tag in gps_tags: if not tag in tags.keys(): return () lng_ref_val = tags[lng_ref_tag_name].values lng_coord_val = [c.decimal() for c in tags[lng_tag_name].values] lat_ref_val = tags[lat_ref_tag_name].values lat_coord_val = [c.decimal() for c in tags[lat_tag_name].values] lng_coord = sum([c/60**i for i, c in enumerate(lng_coord_val)]) lng_coord *= (-1) ** (lng_ref_val == 'W') lat_coord = sum([c/60**i for i, c in enumerate(lat_coord_val)]) lat_coord *= (-1) ** (lat_ref_val == 'S') return (lat_coord, lng_coord) class Ratio(Fraction): """ Ratio object that eventually will be able to reduce itself to lowest common denominator for printing. """ # We're immutable, so use __new__ not __init__ def __new__(cls, numerator=0, denominator=None): try: self = super(Ratio, cls).__new__(cls, numerator, denominator) except ZeroDivisionError: self = super(Ratio, cls).__new__(cls) self._numerator = numerator self._denominator = denominator return self def __repr__(self) -> str: return str(self) @property def num(self): return self.numerator @property def den(self): return self.denominator def decimal(self) -> float: return float(self) exif-py-3.0.0/setup.py000066400000000000000000000022001423577444100146120ustar00rootroot00000000000000from setuptools import setup, find_packages import exifread readme_file = open("README.rst", "rt").read() dev_requirements = [ "mypy==0.950", "pylint==2.13.8", ] setup( name="ExifRead", version=exifread.__version__, author="Ianaré Sévi", author_email="ianare@gmail.com", packages=find_packages(), scripts=["EXIF.py"], url="https://github.com/ianare/exif-py", license="BSD", keywords="exif image metadata photo", description=" ".join(exifread.__doc__.splitlines()).strip(), long_description=readme_file, classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Utilities", ], extras_require={ "dev": dev_requirements, }, )