diff -Nru python-exif-2.3.2/ChangeLog.rst python-exif-3.0.0/ChangeLog.rst --- python-exif-2.3.2/ChangeLog.rst 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/ChangeLog.rst 2022-05-08 17:08:49.000000000 +0000 @@ -2,6 +2,17 @@ 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 @@ -47,7 +58,7 @@ 2.1.0 — 2015-05-15 * Bypass empty/unreadable Olympus MakerNote info (issue #42) - * Suport Apple Makernote and Apple HDR details by Jesus Cea + * 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 @@ -249,14 +260,3 @@ 1999-08-21 TB * Last update by Thierry Bousch to his code. - - - - - - - - - - - diff -Nru python-exif-2.3.2/debian/changelog python-exif-3.0.0/debian/changelog --- python-exif-2.3.2/debian/changelog 2020-11-07 03:05:20.000000000 +0000 +++ python-exif-3.0.0/debian/changelog 2022-07-04 01:26:29.000000000 +0000 @@ -1,3 +1,18 @@ +python-exif (3.0.0-1) unstable; urgency=medium + + [ Diego M. Rodriguez ] + * d/test: mark test as superficial (Closes: #974474) + + [ Debian Janitor ] + * Bump debhelper from old 12 to 13. + * Update standards version to 4.5.1, no changes needed. + + [ TANIGUCHI Takaki ] + * New upstream version 3.0.0 + * Update standards version to 4.6.1, no changes needed. + + -- TANIGUCHI Takaki Mon, 04 Jul 2022 10:26:29 +0900 + python-exif (2.3.2-1) unstable; urgency=medium [ Ondřej Nový ] diff -Nru python-exif-2.3.2/debian/control python-exif-3.0.0/debian/control --- python-exif-2.3.2/debian/control 2020-11-07 03:05:20.000000000 +0000 +++ python-exif-3.0.0/debian/control 2022-07-04 01:26:29.000000000 +0000 @@ -3,8 +3,8 @@ Priority: optional Maintainer: Debian Python Team Uploaders: TANIGUCHI Takaki , W. Martin Borgert -Build-Depends: python3-setuptools, debhelper-compat (= 12), dh-python, python3-all -Standards-Version: 4.5.0 +Build-Depends: python3-setuptools, debhelper-compat (= 13), dh-python, python3-all +Standards-Version: 4.6.1 Homepage: https://github.com/ianare/exif-py Vcs-Git: https://salsa.debian.org/python-team/packages/python-exif.git Vcs-Browser: https://salsa.debian.org/python-team/packages/python-exif diff -Nru python-exif-2.3.2/debian/tests/control python-exif-3.0.0/debian/tests/control --- python-exif-2.3.2/debian/tests/control 2020-11-07 03:05:20.000000000 +0000 +++ python-exif-3.0.0/debian/tests/control 2022-07-04 01:26:29.000000000 +0000 @@ -1,2 +1,3 @@ Depends: @, python3-all Test-Command: set -e ; for py in $(py3versions -r 2>/dev/null) ; do cd "$AUTOPKGTEST_TMP" ; echo "Testing with $py:" ; $py -c "import exifread; print(exifread)" ; done +Restrictions: superficial diff -Nru python-exif-2.3.2/EXIF.py python-exif-3.0.0/EXIF.py --- python-exif-2.3.2/EXIF.py 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/EXIF.py 2022-05-08 17:08:49.000000000 +0000 @@ -1,5 +1,4 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 # # # Library to extract Exif information from digital camera image files. @@ -7,7 +6,7 @@ # # # Copyright (c) 2002-2007 Gene Cash -# Copyright (c) 2007-2020 Ianaré Sévi and contributors +# 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 @@ -19,9 +18,8 @@ import sys import argparse -import logging import timeit -from exifread.tags import DEFAULT_STOP_TAG, FIELD_TYPES +from exifread.tags import FIELD_TYPES from exifread import process_file, exif_log, __version__ logger = exif_log.get_logger() @@ -65,21 +63,17 @@ return args -def main(): - """Parse command line options/arguments and execute.""" - args = get_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: - try: - escaped_fn = escaped_fn = filename.encode( - sys.getfilesystemencoding(), 'surrogateescape' - ).decode() - except UnicodeDecodeError: - #TODO: Python2 specific, remove - escaped_fn = filename + # avoid errors when printing to console + escaped_fn = escaped_fn = filename.encode( + sys.getfilesystemencoding(), 'surrogateescape' + ).decode() file_start = timeit.default_timer() try: @@ -99,7 +93,8 @@ tag_stop = timeit.default_timer() if not data: - logger.warning("No EXIF information found\n") + logger.warning('No EXIF information found') + print() continue if 'JPEGThumbnail' in data: @@ -122,8 +117,8 @@ logger.debug("Tags processed in %s seconds", tag_stop - tag_start) logger.debug("File processed in %s seconds", file_stop - file_start) - print("") + print() if __name__ == '__main__': - main() + main(get_args()) diff -Nru python-exif-2.3.2/exifread/classes.py python-exif-3.0.0/exifread/classes.py --- python-exif-2.3.2/exifread/classes.py 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/exifread/classes.py 2022-05-08 17:08:49.000000000 +0000 @@ -1,27 +1,21 @@ -# -*- coding: utf-8 -*- - -import struct import re +import struct +from typing import BinaryIO, Dict, Any -from .exif_log import get_logger -from .utils import Ratio -from .tags import EXIF_TAGS, DEFAULT_STOP_TAG, FIELD_TYPES, IGNORE_TAGS, makernote +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() -#TODO: Python2 specific, remove -try: - StringCls = basestring -except NameError: - StringCls = str - class IfdTag: """ Eases dealing with tags. """ - def __init__(self, printable, tag, field_type, values, field_offset, field_length): + 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 @@ -32,13 +26,14 @@ self.field_offset = field_offset # length of data field in bytes self.field_length = field_length - # either a string or array of data items + # either string, bytes or list of data items + # TODO: sort out this type mess! self.values = values - def __str__(self): + def __str__(self) -> str: return self.printable - def __repr__(self): + def __repr__(self) -> str: try: tag = '(0x%04X) %s=%s @ %d' % ( self.tag, @@ -46,7 +41,7 @@ self.printable, self.field_offset ) - except: # pylint: disable=bare-except + except TypeError: tag = '(%s) %s=%s @ %s' % ( str(self.tag), FIELD_TYPES[self.field_type][2], @@ -60,7 +55,8 @@ """ Handle an EXIF header. """ - def __init__(self, file_handle, endian, offset, fake_exif, strict, + + 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 @@ -70,9 +66,10 @@ self.debug = debug self.detailed = detailed self.truncate_tags = truncate_tags - self.tags = {} + # TODO: get rid of 'Any' type + self.tags = {} # type: Dict[str, Any] - def s2n(self, offset, length, signed=False): + def s2n(self, offset, length: int, signed=False) -> int: """ Convert slice to integer, based on sign and endian flags. @@ -96,30 +93,34 @@ (8, False): 'L', (8, True): 'l', }[(length, signed)] - except KeyError: - raise ValueError('unexpected unpacking length: %d' % length) + 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 n2s(self, offset, length): - """Convert offset to string.""" - s = '' + def n2b(self, offset, length) -> bytes: + """Convert offset to bytes.""" + s = b'' for _ in range(length): if self.endian == 'I': - s += chr(offset & 0xFF) + s += bytes([offset & 0xFF]) else: - s = chr(offset & 0xFF) + s + s = bytes([offset & 0xFF]) + s offset = offset >> 8 return s - def _first_ifd(self): + def _first_ifd(self) -> int: """Return first IFD.""" return self.s2n(4, 4) - def _next_ifd(self, ifd): + 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) @@ -127,11 +128,16 @@ return 0 return next_ifd - def list_ifd(self): + 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 @@ -163,7 +169,12 @@ unpack_format += 'd' self.file_handle.seek(self.offset + offset) byte_str = self.file_handle.read(type_length) - value = struct.unpack(unpack_format, byte_str) + 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) @@ -203,7 +214,7 @@ values = '' return values - def _process_tag(self, ifd, ifd_name, tag_entry, entry, tag, tag_name, relative, stop_tag): + 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 @@ -241,21 +252,17 @@ 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, StringCls): + 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: - #TODO: Python2 specific, remove - try: - printable = str(values) - except UnicodeEncodeError: - printable = unicode(values) # pylint: disable=undefined-variable + printable = str(values) # compute printable version of values if tag_entry: # optional 2nd tag element is present @@ -279,16 +286,10 @@ self.tags[ifd_name + ' ' + tag_name] = IfdTag( printable, tag, field_type, values, field_offset, count * type_length ) - try: - tag_value = repr(self.tags[ifd_name + ' ' + tag_name]) - #TODO: Python2 specific, remove - except UnicodeEncodeError: - tag_value = unicode( # pylint: disable=undefined-variable - self.tags[ifd_name + ' ' + tag_name] - ) + tag_value = repr(self.tags[ifd_name + ' ' + tag_name]) logger.debug(' %s: %s', tag_name, tag_value) - def dump_ifd(self, ifd, ifd_name, tag_dict=None, relative=0, stop_tag=DEFAULT_STOP_TAG): + 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. """ @@ -320,7 +321,7 @@ if tag_name == stop_tag: break - def extract_tiff_thumbnail(self, thumb_ifd): + def extract_tiff_thumbnail(self, thumb_ifd: int) -> None: """ Extract uncompressed TIFF thumbnail. @@ -334,12 +335,12 @@ entries = self.s2n(thumb_ifd, 2) # this is header plus offset to IFD ... if self.endian == 'M': - tiff = 'MM\x00*\x00\x00\x00\x08' + tiff = b'MM\x00*\x00\x00\x00\x08' else: - tiff = 'II*\x00\x08\x00\x00\x00' + 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) + '\x00\x00\x00\x00' + 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): @@ -360,7 +361,7 @@ # 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.n2s(newoff, 4) + tiff[ptr + 4:] + tiff = tiff[:ptr] + self.n2b(newoff, 4) + tiff[ptr + 4:] # remember strip offsets location if tag == 0x0111: strip_off = newoff @@ -374,7 +375,7 @@ 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.n2s(len(tiff), strip_len) + 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 @@ -383,7 +384,7 @@ self.tags['TIFFThumbnail'] = tiff - def extract_jpeg_thumbnail(self): + def extract_jpeg_thumbnail(self) -> None: """ Extract JPEG thumbnail. @@ -403,7 +404,7 @@ 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): + def decode_maker_note(self) -> None: """ Decode all the camera-specific MakerNote formats @@ -424,6 +425,8 @@ 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'] @@ -487,8 +490,7 @@ return # Apple - if make == 'Apple' and \ - note.values[0:10] == [65, 112, 112, 108, 101, 32, 105, 79, 83, 0]: + 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) @@ -520,7 +522,6 @@ # 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. @@ -539,10 +540,9 @@ 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), None, 0, None, - None, None) + # 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): """ @@ -553,7 +553,7 @@ return model = str(model.values) - camera_info_tags = None + 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 @@ -565,8 +565,7 @@ # 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) + 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(): @@ -585,19 +584,27 @@ 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), None, 0, None, None, None) + self.tags['MakerNote ' + tag_name] = IfdTag(str(tag_value), 0, 0, tag_value, 0, 0) - def parse_xmp(self, xmp_string): + 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') + 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") - xml = xml.dom.minidom.parseString(xmp_string) - pretty = xml.toprettyxml() + 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), None, 1, None, None, None) + self.tags['Image ApplicationNotes'] = IfdTag('\n'.join(cleaned), 0, 1, xmp_bytes, 0, 0) diff -Nru python-exif-2.3.2/exifread/exceptions.py python-exif-3.0.0/exifread/exceptions.py --- python-exif-2.3.2/exifread/exceptions.py 1970-01-01 00:00:00.000000000 +0000 +++ python-exif-3.0.0/exifread/exceptions.py 2022-05-08 17:08:49.000000000 +0000 @@ -0,0 +1,6 @@ +class InvalidExif(Exception): + pass + + +class ExifNotFound(Exception): + pass diff -Nru python-exif-2.3.2/exifread/exif_log.py python-exif-3.0.0/exifread/exif_log.py --- python-exif-2.3.2/exifread/exif_log.py 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/exifread/exif_log.py 2022-05-08 17:08:49.000000000 +0000 @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ Custom log output """ @@ -19,7 +17,7 @@ def get_logger(): """Use this from all files needing to log.""" - return logging.getLogger('exifread') + return logging.getLogger("exifread") def setup_logger(debug, color): @@ -30,7 +28,7 @@ else: log_level = logging.INFO - logger = logging.getLogger('exifread') + logger = logging.getLogger("exifread") stream = Handler(log_level, debug, color) logger.addHandler(stream) logger.setLevel(log_level) @@ -45,9 +43,9 @@ self.color = color self.debug = debug if self.debug: - log_format = '%(levelname)-6s %(message)s' + log_format = "%(levelname)-6s %(message)s" else: - log_format = '%(message)s' + log_format = "%(message)s" logging.Formatter.__init__(self, log_format) def format(self, record): @@ -69,7 +67,6 @@ class Handler(logging.StreamHandler): - def __init__(self, log_level, debug=False, color=False): self.color = color self.debug = debug @@ -77,6 +74,7 @@ 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) diff -Nru python-exif-2.3.2/exifread/heic.py python-exif-3.0.0/exifread/heic.py --- python-exif-2.3.2/exifread/heic.py 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/exifread/heic.py 2022-05-08 17:08:49.000000000 +0000 @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Find Exif data in an HEIC file. # As of 2019, the latest standard seems to be "ISO/IEC 14496-12:2015" @@ -13,11 +11,13 @@ # gives us position and size information. import struct +from typing import List, Dict, Callable, BinaryIO, Optional -from .exif_log import get_logger +from exifread.exif_log import get_logger logger = get_logger() + class WrongBox(Exception): pass class NoParser(Exception): @@ -28,29 +28,62 @@ pass -class Box: # pylint: disable=too-few-public-methods - def __init__(self, name): +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 - self.size = None - self.after = None - self.pos = None - self.item_id = None - def __repr__(self): + 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): + def __init__(self, file_handle: BinaryIO): self.file_handle = file_handle - def get(self, nbytes): + 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 postion {pos}".format( + msg = "get(nbytes={nbytes}) found {read} bytes at position {pos}".format( nbytes=nbytes, read=len(read), pos=self.file_handle.tell() @@ -58,23 +91,23 @@ raise BadSize(msg) return read - def get16(self): + def get16(self) -> int: return struct.unpack('>H', self.get(2))[0] - def get32(self): + def get32(self) -> int: return struct.unpack('>L', self.get(4))[0] - def get64(self): + def get64(self) -> int: return struct.unpack('>Q', self.get(8))[0] - def get_int4x2(self): + def get_int4x2(self) -> tuple: num = struct.unpack('>B', self.get(1))[0] num0 = num >> 4 num1 = num & 0xf return num0, num1 - # some fields have variant-sized data. - def get_int(self, size): + def get_int(self, size: int) -> int: + """some fields have variant-sized data.""" if size == 2: return self.get16() if size == 4: @@ -85,7 +118,7 @@ return 0 raise BadSize(size) - def get_string(self): + def get_string(self) -> bytes: read = [] while 1: char = self.get(1) @@ -94,7 +127,7 @@ read.append(char) return b''.join(read) - def next_box(self): + def next_box(self) -> Box: pos = self.file_handle.tell() size = self.get32() kind = self.get(4).decode('ascii') @@ -113,37 +146,40 @@ box.pos = self.file_handle.tell() return box - def get_full(self, box): - # iso boxes come in 'old' and 'full' variants. the 'full' variant - # contains version and flags information. - vflags = self.get32() - box.version = vflags >> 24 - box.flags = vflags & 0x00ffffff + def get_full(self, box: Box): + box.set_full(self.get32()) - def skip(self, box): + def skip(self, box: Box): self.file_handle.seek(box.after) - def expect_parse(self, name): + 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): - method = 'parse_%s' % box.name - return getattr(self, method, None) + 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): + def parse_box(self, box: Box) -> Box: probe = self.get_parser(box) - if probe is None: - raise NoParser(box.name) probe(box) # in case anything is left unread self.file_handle.seek(box.after) return box - def parse_ftyp(self, box): + def _parse_ftyp(self, box: Box): box.major_brand = self.get(4) box.minor_version = self.get32() box.compat = [] @@ -152,10 +188,8 @@ box.compat.append(self.get(4)) size -= 4 - def parse_meta(self, meta): + def _parse_meta(self, meta: Box): self.get_full(meta) - # this is full of boxes, but not in a predictable order. - meta.subs = {} while self.file_handle.tell() < meta.after: box = self.next_box() psub = self.get_parser(box) @@ -167,7 +201,7 @@ # skip any unparsed data self.skip(box) - def parse_infe(self, box): + def _parse_infe(self, box: Box): self.get_full(box) if box.version >= 2: if box.version == 2: @@ -178,10 +212,8 @@ box.item_type = self.get(4) box.item_name = self.get_string() # ignore the rest - else: - box.item_type = '' - def parse_iinf(self, box): + def _parse_iinf(self, box: Box): self.get_full(box) count = self.get16() box.exif_infe = None @@ -192,14 +224,11 @@ box.exif_infe = infe break - def parse_iloc(self, box): + def _parse_iloc(self, box: Box): self.get_full(box) size0, size1 = self.get_int4x2() size2, size3 = self.get_int4x2() - box.offset_size = size0 - box.length_size = size1 - box.base_offset_size = size2 - box.index_size = size3 + box.set_sizes(size0, size1, size2, size3) if box.version < 2: box.item_count = self.get16() elif box.version == 2: @@ -232,11 +261,12 @@ extents.append((extent_offset, extent_length)) box.locs[item_id] = extents - def find_exif(self): + 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.') diff -Nru python-exif-2.3.2/exifread/__init__.py python-exif-3.0.0/exifread/__init__.py --- python-exif-2.3.2/exifread/__init__.py 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/exifread/__init__.py 2022-05-08 17:08:49.000000000 +0000 @@ -3,29 +3,22 @@ """ import struct +from typing import BinaryIO -from .exif_log import get_logger -from .classes import ExifHeader -from .tags import DEFAULT_STOP_TAG -from .utils import ord_, make_string -from .heic import HEICExifFinder +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__ = '2.3.2' +__version__ = '3.0.0' logger = get_logger() -class InvalidExif(Exception): - pass - -class ExifNotFound(Exception): - pass - -def increment_base(data, base): - return ord_(data[base + 2]) * 256 + ord_(data[base + 3]) + 2 - - -def _find_tiff_exif(fh): +def _find_tiff_exif(fh: BinaryIO) -> tuple: logger.debug("TIFF format recognized in data[0:2]") fh.seek(0) endian = fh.read(1) @@ -34,7 +27,7 @@ return offset, endian -def _find_webp_exif(fh): +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) @@ -44,153 +37,39 @@ while True: data = fh.read(8) # Chunk FourCC (32 bits) and Chunk Size (32 bits) if len(data) != 8: - logger.debug("Invalid webp file chunk header.") - raise InvalidExif() + 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(' 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) +def _find_png_exif(fh: BinaryIO, data: bytes) -> tuple: + logger.debug("PNG format recognized in data[0:8]=%s", data[:8].hex()) + fh.seek(8) - # 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 = 2 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) + data = fh.read(8) + chunk = data[4:8] + logger.debug("PNG found chunk %s", chunk.decode("ascii")) + + if chunk in (b'', b'IEND'): 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: - logger.debug(" Unexpected/unhandled segment type or file content.") - raise InvalidExif() - else: - logger.debug(" Increment base by %s", increment) - base += increment - 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 - logger.debug("No EXIF header expected data[2+base]==0xFF and data[6+base:10+base]===Exif (or Duck)") - logger.debug("Did get 0x%X and %s", ord_(data[2 + base]), data[6 + base:10 + base + 1]) - raise ExifNotFound() - return offset, endian, fake_exif + 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) -def _get_xmp(fh): - xmp_string = b'' + 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 @@ -209,29 +88,17 @@ line = line[:(close_tag - line_offset) + 12] xml_finished = True if xml_started: - xmp_string += line + xmp_bytes += line if xml_finished: break logger.debug('XMP Finished searching for info') - return xmp_string - + return xmp_bytes -def process_file(fh, 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. - """ +def _determine_type(fh: BinaryIO) -> tuple: # by default do not fake an EXIF beginning fake_exif = 0 - if auto_seek: - fh.seek(0) - - # determine the file type data = fh.read(12) if data[0:2] in [b'II', b'MM']: # it's a TIFF file @@ -241,19 +108,38 @@ heic = HEICExifFinder(fh) offset, endian = heic.find_exif() elif data[0:4] == b'RIFF' and data[8:12] == b'WEBP': - try: - offset, endian = _find_webp_exif(fh) - except (InvalidExif, ExifNotFound): - return {} + offset, endian = _find_webp_exif(fh) elif data[0:2] == b'\xFF\xD8': # it's a JPEG file - try: - offset, endian, fake_exif = _find_jpeg_exif(fh, data, fake_exif) - except (InvalidExif, ExifNotFound): - return {} + 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 - logger.debug("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])) @@ -267,7 +153,7 @@ hdr = ExifHeader(fh, endian, offset, fake_exif, strict, debug, details, truncate_tags) ifd_list = hdr.list_ifd() - thumb_ifd = False + thumb_ifd = 0 ctr = 0 for ifd in ifd_list: if ctr == 0: @@ -299,15 +185,14 @@ # parse XMP tags (experimental) if debug and details: - xmp_string = b'' # Easy we already have them - if 'Image ApplicationNotes' in hdr.tags: + xmp_tag = hdr.tags.get('Image ApplicationNotes') + if xmp_tag: logger.debug('XMP present in Exif') - xmp_string = make_string(hdr.tags['Image ApplicationNotes'].values) + xmp_bytes = bytes(xmp_tag.values) # We need to look in the entire file for the XML else: - xmp_string = _get_xmp(fh) - if xmp_string: - hdr.parse_xmp(xmp_string) - + xmp_bytes = _get_xmp(fh) + if xmp_bytes: + hdr.parse_xmp(xmp_bytes) return hdr.tags diff -Nru python-exif-2.3.2/exifread/jpeg.py python-exif-3.0.0/exifread/jpeg.py --- python-exif-2.3.2/exifread/jpeg.py 1970-01-01 00:00:00.000000000 +0000 +++ python-exif-3.0.0/exifread/jpeg.py 2022-05-08 17:08:49.000000000 +0000 @@ -0,0 +1,155 @@ +from 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 diff -Nru python-exif-2.3.2/exifread/tags/exif.py python-exif-3.0.0/exifread/tags/exif.py --- python-exif-2.3.2/exifread/tags/exif.py 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/exifread/tags/exif.py 2022-05-08 17:08:49.000000000 +0000 @@ -2,7 +2,7 @@ Standard tag definitions. """ -from ..utils import make_string, make_string_uc +from exifread.utils import make_string, make_string_uc # Interoperability tags INTEROP_TAGS = { @@ -181,6 +181,7 @@ 1: 'Regenerated', 2: 'Unclean' }), + 0x014A: ('SubIFDs', ), 0x0148: ('ConsecutiveBadFaxLines', ), 0x014C: ('InkSet', { 1: 'CMYK', @@ -207,9 +208,10 @@ 0x0155: ('SMaxSampleValue', ), 0x0156: ('TransferRange', ), 0x0157: ('ClipPath', ), + 0x015B: ('JPEGTables', ), 0x0200: ('JPEGProc', ), - 0x0201: ('JPEGInterchangeFormat', ), - 0x0202: ('JPEGInterchangeFormatLength', ), + 0x0201: ('JPEGInterchangeFormat', ), # JpegIFOffset + 0x0202: ('JPEGInterchangeFormatLength', ), # JpegIFByteCount 0x0211: ('YCbCrCoefficients', ), 0x0212: ('YCbCrSubSampling', ), 0x0213: ('YCbCrPositioning', { @@ -243,6 +245,9 @@ 0x8825: ('GPSInfo', GPS_INFO), # GPS tags 0x8827: ('ISOSpeedRatings', ), 0x8828: ('OECF', ), + 0x8829: ('Interlace', ), + 0x882A: ('TimeZoneOffset', ), + 0x882B: ('SelfTimerMode', ), 0x8830: ('SensitivityType', { 0: 'Unknown', 1: 'Standard Output Sensitivity', @@ -335,7 +340,15 @@ 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', ), @@ -437,10 +450,9 @@ 0xA435: ('LensSerialNumber', ), 0xA500: ('Gamma', ), 0xC4A5: ('PrintIM', ), + 0xC61A: ('BlackLevel', ), 0xEA1C: ('Padding', ), 0xEA1D: ('OffsetSchema', ), 0xFDE8: ('OwnerName', ), 0xFDE9: ('SerialNumber', ), - 0xC61A: ('BlackLevel', ), - } diff -Nru python-exif-2.3.2/exifread/tags/__init__.py python-exif-3.0.0/exifread/tags/__init__.py --- python-exif-2.3.2/exifread/tags/__init__.py 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/exifread/tags/__init__.py 2022-05-08 17:08:49.000000000 +0000 @@ -2,8 +2,10 @@ Tag definitions """ -from .exif import EXIF_TAGS -from .makernote import apple, canon, casio, fujifilm, nikon, olympus +from exifread.tags.exif import EXIF_TAGS +from exifread.tags.makernote import ( + apple, canon, casio, fujifilm, nikon, olympus, +) DEFAULT_STOP_TAG = 'UNDEF' @@ -27,7 +29,7 @@ # To ignore when quick processing IGNORE_TAGS = ( - 0x9286, # user comment - 0x927C, # MakerNote Tags 0x02BC, # XPM + 0x927C, # MakerNote Tags + 0x9286, # user comment ) diff -Nru python-exif-2.3.2/exifread/tags/makernote/fujifilm.py python-exif-3.0.0/exifread/tags/makernote/fujifilm.py --- python-exif-2.3.2/exifread/tags/makernote/fujifilm.py 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/exifread/tags/makernote/fujifilm.py 2022-05-08 17:08:49.000000000 +0000 @@ -4,7 +4,7 @@ http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/FujiFilm.html """ -from ...utils import make_string +from exifread.utils import make_string TAGS = { 0x0000: ('NoteVersion', make_string), diff -Nru python-exif-2.3.2/exifread/tags/makernote/nikon.py python-exif-3.0.0/exifread/tags/makernote/nikon.py --- python-exif-2.3.2/exifread/tags/makernote/nikon.py 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/exifread/tags/makernote/nikon.py 2022-05-08 17:08:49.000000000 +0000 @@ -1,7 +1,8 @@ -from ...utils import make_string, Ratio +from exifread.utils import make_string, Ratio -def ev_bias(seq): + +def ev_bias(seq) -> str: """ First digit seems to be in steps of 1/6 EV. Does the third value mean the step size? It is usually 6, @@ -49,10 +50,11 @@ ret_str = ret_str + str(ratio) + ' EV' return ret_str + # Nikon E99x MakerNote Tags TAGS_NEW = { 0x0001: ('MakernoteVersion', make_string), # Sometimes binary - 0x0002: ('ISOSetting', make_string), + 0x0002: ('ISOSetting', ), 0x0003: ('ColorMode', ), 0x0004: ('Quality', ), 0x0005: ('Whitebalance', ), diff -Nru python-exif-2.3.2/exifread/tags/makernote/olympus.py python-exif-3.0.0/exifread/tags/makernote/olympus.py --- python-exif-2.3.2/exifread/tags/makernote/olympus.py 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/exifread/tags/makernote/olympus.py 2022-05-08 17:08:49.000000000 +0000 @@ -1,5 +1,6 @@ -from ...utils import make_string +from exifread.utils import make_string + def special_mode(val): """Decode Olympus SpecialMode tag in MakerNote""" @@ -16,9 +17,13 @@ 3: 'Bottom to top', 4: 'Top to bottom', } - if not val or (val[0] not in mode1 or val[2] not in mode2): + + if not val: return val - return '%s - sequence %d - %s' % (mode1[val[0]], val[1], mode2[val[2]]) + + 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 = { diff -Nru python-exif-2.3.2/exifread/utils.py python-exif-3.0.0/exifread/utils.py --- python-exif-2.3.2/exifread/utils.py 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/exifread/utils.py 2022-05-08 17:08:49.000000000 +0000 @@ -1,15 +1,9 @@ -# -*- coding: utf-8 -*- - """ Misc utilities. """ from fractions import Fraction - -try: - StringCls = basestring -except NameError: - StringCls = str +from typing import Union def ord_(dta): @@ -18,7 +12,7 @@ return dta -def make_string(seq): +def make_string(seq: Union[bytes, list]) -> str: """ Don't throw an exception when given an out of range character. """ @@ -30,25 +24,36 @@ string += chr(char) except TypeError: pass - # If no printing chars + + # If no printing chars if not string: - return str(seq) - return 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): +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, StringCls): - seq = seq[8:] + 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): +def get_gps_coords(tags: dict) -> tuple: lng_ref_tag_name = 'GPS GPSLongitudeRef' lng_tag_name = 'GPS GPSLongitude' @@ -59,7 +64,7 @@ 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 None + return () lng_ref_val = tags[lng_ref_tag_name].values lng_coord_val = [c.decimal() for c in tags[lng_tag_name].values] @@ -91,9 +96,8 @@ self._numerator = numerator self._denominator = denominator return self - __new__.doc = Fraction.__new__.__doc__ - def __repr__(self): + def __repr__(self) -> str: return str(self) @property @@ -104,5 +108,5 @@ def den(self): return self.denominator - def decimal(self): + def decimal(self) -> float: return float(self) diff -Nru python-exif-2.3.2/.github/workflows/linting.yml python-exif-3.0.0/.github/workflows/linting.yml --- python-exif-2.3.2/.github/workflows/linting.yml 1970-01-01 00:00:00.000000000 +0000 +++ python-exif-3.0.0/.github/workflows/linting.yml 2022-05-08 17:08:49.000000000 +0000 @@ -0,0 +1,43 @@ +# +# 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 diff -Nru python-exif-2.3.2/.github/workflows/test.yml python-exif-3.0.0/.github/workflows/test.yml --- python-exif-2.3.2/.github/workflows/test.yml 1970-01-01 00:00:00.000000000 +0000 +++ python-exif-3.0.0/.github/workflows/test.yml 2022-05-08 17:08:49.000000000 +0000 @@ -0,0 +1,54 @@ +# +# 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 diff -Nru python-exif-2.3.2/__init__.py python-exif-3.0.0/__init__.py --- python-exif-2.3.2/__init__.py 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -# diff -Nru python-exif-2.3.2/LICENSE.txt python-exif-3.0.0/LICENSE.txt --- python-exif-2.3.2/LICENSE.txt 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/LICENSE.txt 2022-05-08 17:08:49.000000000 +0000 @@ -1,6 +1,6 @@ Copyright (c) 2002-2007 Gene Cash -Copyright (c) 2007-2020 Ianaré Sévi and contributors +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 diff -Nru python-exif-2.3.2/Makefile python-exif-3.0.0/Makefile --- python-exif-2.3.2/Makefile 1970-01-01 00:00:00.000000000 +0000 +++ python-exif-3.0.0/Makefile 2022-05-08 17:08:49.000000000 +0000 @@ -0,0 +1,55 @@ + +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 diff -Nru python-exif-2.3.2/.pylintrc python-exif-3.0.0/.pylintrc --- python-exif-2.3.2/.pylintrc 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/.pylintrc 2022-05-08 17:08:49.000000000 +0000 @@ -55,84 +55,18 @@ # 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 reenable specific checks. For example, if +# 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=parameter-unpacking, - unpacking-in-except, - backtick, - long-suffix, - raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - use-symbolic-message-instead, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - missing-docstring, - # some magic stuff going on, look into this - import-error, +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, - too-many-statements, # 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 @@ -535,7 +469,7 @@ max-bool-expr=5 # Maximum number of branch for function / method body. -max-branches=12 +max-branches=10 # Maximum number of locals for function / method body. max-locals=20 diff -Nru python-exif-2.3.2/README.rst python-exif-3.0.0/README.rst --- python-exif-2.3.2/README.rst 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/README.rst 2022-05-08 17:08:49.000000000 +0000 @@ -2,31 +2,17 @@ EXIF.py ******* -.. image:: https://travis-ci.org/ianare/exif-py.png - :target: https://travis-ci.org/ianare/exif-py - Easy to use Python module to extract Exif metadata from digital image files. -Supported formats: - -- Python2 & Python3: TIFF, JPEG -- Python3 only: WebP, HEIC +Supported formats: TIFF, JPEG, PNG, Webp, HEIC Compatibility ************* -EXIF.py is tested on the following Python versions: - -- 2.7 -- 3.5 -- 3.6 -- 3.7 -- 3.8 +EXIF.py is tested and officially supported on Python 3.5 to 3.10 -As of version 2.3.0, Python2 is no longer offcially supported. - -**!! Version 2.x of EXIF.py will be the last to work with Python2 !!** +Starting with version ``3.0.0``, Python2 compatibility is dropped *completely* (syntax errors due to type hinting). https://pythonclock.org/ @@ -34,8 +20,8 @@ Installation ************ -PyPI -==== +Stable Version +============== The recommended process is to install the `PyPI package `_, as it allows easily staying up to date:: @@ -43,12 +29,16 @@ See the `pip documentation `_ for more info. -Archive -======= -Download an archive from the project's `releases page `_. +EXIF.py is mature software and strives for stability. -Extract and enjoy. +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 ***** @@ -58,13 +48,13 @@ Some examples:: - $ EXIF.py image1.jpg - $ EXIF.py -dc image1.jpg image2.tiff - $ find ~/Pictures -name "*.jpg" -o -name "*.tiff" | xargs EXIF.py + 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 + EXIF.py -h Python Script ============= @@ -72,7 +62,7 @@ .. code-block:: python import exifread - # Open image file for reading (binary mode) + # Open image file for reading (must be in binary mode) f = open(path_name, 'rb') # Return Exif tags @@ -166,7 +156,8 @@ .. code-block:: python import exifread - from Pillow import Image + from PIL import Image + import logging def _read_img_and_correct_exif_orientation(path): im = Image.open(path) @@ -175,12 +166,15 @@ 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,8] - if 7 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) diff -Nru python-exif-2.3.2/setup.py python-exif-3.0.0/setup.py --- python-exif-2.3.2/setup.py 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/setup.py 2022-05-08 17:08:49.000000000 +0000 @@ -1,10 +1,13 @@ -# -*- coding: utf-8 -*- - from 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__, @@ -24,11 +27,13 @@ "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", - "Programming Language :: Python :: 2.7", "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, + }, ) diff -Nru python-exif-2.3.2/.travis.yml python-exif-3.0.0/.travis.yml --- python-exif-2.3.2/.travis.yml 2020-10-29 19:29:15.000000000 +0000 +++ python-exif-3.0.0/.travis.yml 1970-01-01 00:00:00.000000000 +0000 @@ -1,41 +0,0 @@ -language: python -cache: pip -dist: xenial - -matrix: - include: - - python: 2.7 - before_install: "" - before_script: "" - script: - - "wget https://github.com/ianare/exif-samples/archive/master.tar.gz" - - "tar -xzf master.tar.gz" - - "find exif-samples-master -name *.tiff -o -name *.jpg | sort -f | xargs EXIF.py" - - "find exif-samples-master -name *.tiff -o -name *.jpg | xargs EXIF.py -dc" - - python: 3.5 - before_install: "" - before_script: "" - - python: 3.6 - - python: 3.7 - - python: 3.8 - dist: bionic - -before_install: - - "pip install pylint==2.5.3" - -install: - - "pip install ." - -before_script: - # stop here if linting fails - - "pylint ./exifread" - -script: - # get the test images - - "wget https://github.com/ianare/exif-samples/archive/master.tar.gz" - - "tar -xzf master.tar.gz" - # could be much improved ... - - "find exif-samples-master -name *.tiff -o -name *.jpg | sort -f | xargs EXIF.py > exif-samples-master/dump_test" - - "diff -y --suppress-common-lines exif-samples-master/dump exif-samples-master/dump_test" - # test colors - - "find exif-samples-master -name *.tiff -o -name *.jpg | xargs EXIF.py -dc"