Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a serialization option for EXIF dictionaries #129

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions ChangeLog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* fix webp for file conversion by magick (#132) by Marcelo
* Ignore unknown parsers, fix for #160 (#175) by Herbert Poul
* Allow extracting thumbnails with details=False (#170)
* Add option to return built-in Python types (#129) by Étienne Pelletier

3.0.0 — 2022-05-08
* **BREAKING CHANGE:** Add type hints, which removes Python2 compatibility
Expand Down
20 changes: 13 additions & 7 deletions EXIF.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ def get_args() -> argparse.Namespace:
'-s', '--strict', action='store_true', dest='strict',
help='Run in strict mode (stop on errors).',
)
parser.add_argument(
'-b', '--builtin', action='store_true', dest='builtin_types',
help='Convert IfdTag values to built-in Python variable types',
)
parser.add_argument(
'-d', '--debug', action='store_true', dest='debug',
help='Run in debug mode (display extra info).',
Expand Down Expand Up @@ -92,7 +96,8 @@ def main(args) -> None:
details=args.detailed,
strict=args.strict,
debug=args.debug,
extract_thumbnail=args.detailed
extract_thumbnail=args.detailed,
builtin_types=args.builtin_types,
)

tag_stop = timeit.default_timer()
Expand All @@ -109,14 +114,15 @@ def main(args) -> None:
logger.info('File has TIFF thumbnail')
del data['TIFFThumbnail']

tag_keys = list(data.keys())
tag_keys.sort()

for i in tag_keys:
for field in sorted(data):
value = data[field]
try:
logger.info('%s (%s): %s', i, FIELD_TYPES[data[i].field_type][2], data[i].printable)
if args.builtin_types:
logger.info('%s (%s): %r', field, type(value).__name__, value)
else:
logger.info('%s (%s): %s', field, FIELD_TYPES[value.field_type][2], value.printable)
except:
logger.error("%s : %s", i, str(data[i]))
logger.error("%s: %s", field, str(value))

file_stop = timeit.default_timer()

Expand Down
25 changes: 21 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ You can process the tags as you wish. In particular, you can iterate through all

.. code-block:: python

for tag in tags.keys():
for tag, value in tags.items():
if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename', 'EXIF MakerNote'):
print "Key: %s, value %s" % (tag, tags[tag])
print(f"Key: {tag}, value {value}")

An ``if`` statement is used to avoid printing out a few of the tags that tend to be long or boring.

Expand Down Expand Up @@ -160,6 +160,23 @@ Pass the ``-s`` or ``--strict`` argument, or as:

tags = exifread.process_file(file_handle, strict=True)

Built-in Types
==============

For easier serialization and programmatic use, this option returns a dictionary with values in built-in Python types (int, float, str, bytes, list, None) instead of `IfdTag` objects.

Pass the ``-b`` or ``--builtin`` argument, or as:

.. code-block:: python

tags = exifread.process_file(file_handle, builtin_types=True)

For direct JSON serialization, combine this option with ``details=False`` to avoid bytes in the output:

.. code-block:: python

json.dumps(exifread.process_file(file_handle, details=False, builtin_types=True))

Usage Example
=============

Expand All @@ -171,14 +188,14 @@ This example shows how to use the library to correct the orientation of an image
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 file_handle:
tags = exifread.process_file(file_handle, details=False)

if "Image Orientation" in tags.keys():
if "Image Orientation" in tags:
orientation = tags["Image Orientation"]
logging.basicConfig(level=logging.DEBUG)
logging.debug("Orientation: %s (%s)", orientation, orientation.values)
Expand Down
11 changes: 9 additions & 2 deletions exifread/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""
Read Exif metadata from tiff and jpeg files.
Read Exif metadata from image files
Supported formats: TIFF, JPEG, PNG, Webp, HEIC
"""

import struct
Expand All @@ -12,6 +13,7 @@
from exifread.heic import HEICExifFinder
from exifread.jpeg import find_jpeg_exif
from exifread.exceptions import InvalidExif, ExifNotFound
from exifread.serialize import convert_types

__version__ = '3.1.0'

Expand Down Expand Up @@ -122,7 +124,8 @@ def _determine_type(fh: BinaryIO) -> tuple:

def process_file(fh: BinaryIO, stop_tag=DEFAULT_STOP_TAG,
details=True, strict=False, debug=False,
truncate_tags=True, auto_seek=True, extract_thumbnail=True):
truncate_tags=True, auto_seek=True,
extract_thumbnail=True, builtin_types=False) -> dict:
"""
Process an image file (expects an open file object).

Expand Down Expand Up @@ -195,4 +198,8 @@ def process_file(fh: BinaryIO, stop_tag=DEFAULT_STOP_TAG,
xmp_bytes = _get_xmp(fh)
if xmp_bytes:
hdr.parse_xmp(xmp_bytes)

if builtin_types:
return convert_types(hdr.tags)

return hdr.tags
11 changes: 9 additions & 2 deletions exifread/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class IfdTag:
"""

def __init__(self, printable: str, tag: int, field_type: int, values,
field_offset: int, field_length: int):
field_offset: int, field_length: int, prefer_printable: bool = True):
# printable version of data
self.printable = printable
# tag ID number
Expand All @@ -29,6 +29,8 @@ def __init__(self, printable: str, tag: int, field_type: int, values,
# either string, bytes or list of data items
# TODO: sort out this type mess!
self.values = values
# indication if printable version should be used upon serialization
self.prefer_printable = prefer_printable

def __str__(self) -> str:
return self.printable
Expand Down Expand Up @@ -263,10 +265,15 @@ def _process_tag(self, ifd, ifd_name: str, tag_entry, entry, tag: int, tag_name,
printable = str(values[0:-1])
else:
printable = str(values)

prefer_printable = False

# compute printable version of values
if tag_entry:
# optional 2nd tag element is present
if len(tag_entry) != 1:
prefer_printable = True

if callable(tag_entry[1]):
# call mapping function
printable = tag_entry[1](values)
Expand All @@ -284,7 +291,7 @@ def _process_tag(self, ifd, ifd_name: str, tag_entry, entry, tag: int, tag_name,
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
printable, tag, field_type, values, field_offset, count * type_length, prefer_printable
)
tag_value = repr(self.tags[ifd_name + ' ' + tag_name])
logger.debug(' %s: %s', tag_name, tag_value)
Expand Down
2 changes: 1 addition & 1 deletion exifread/exif_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def setup_logger(debug, color):
else:
log_level = logging.INFO

logger = logging.getLogger("exifread")
logger = get_logger()
stream = Handler(log_level, debug, color)
logger.addHandler(stream)
logger.setLevel(log_level)
Expand Down
Loading
Loading