diff --git a/exifread/__init__.py b/exifread/__init__.py index 6e9cb57..4ce1f28 100644 --- a/exifread/__init__.py +++ b/exifread/__init__.py @@ -13,7 +13,7 @@ from exifread.jpeg import find_jpeg_exif from exifread.exceptions import InvalidExif, ExifNotFound -__version__ = '3.0.0' +__version__ = '3.1.0.dev1' logger = get_logger() diff --git a/exifread/classes.py b/exifread/classes.py index e9113f5..57768b4 100644 --- a/exifread/classes.py +++ b/exifread/classes.py @@ -1,9 +1,9 @@ import re import struct +from fractions import Fraction 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() @@ -152,10 +152,14 @@ def _process_field(self, tag_name, count, field_type, type_length, offset): for _ in range(count): if field_type in (5, 10): # a ratio - value = Ratio( + value = [ self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed) - ) + ] + try: + value = Fraction(*value) + except ZeroDivisionError: + pass elif field_type in (11, 12): # a float or double unpack_format = '' @@ -496,60 +500,56 @@ def decode_maker_note(self) -> None: self.dump_ifd(0, 'MakerNote', tag_dict=makernote.apple.TAGS) self.offset = offset return - + if make == 'DJI': offset = self.offset self.offset += note.field_offset self.dump_ifd(0, 'MakerNote', tag_dict=makernote.dji.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] + for tag_name, tag_def in (('MakerNote CameraSettings', makernote.canon.CAMERA_SETTINGS), + ('MakerNote FocalLength', makernote.canon.FOCAL_LENGTH), + ('MakerNote ShotInfo', makernote.canon.SHOT_INFO), + ('MakerNote AFInfo2', makernote.canon.AF_INFO_2), + ('MakerNote FileInfo', makernote.canon.FILE_INFO)): + if tag_name in self.tags: + logger.debug('Canon %s', tag_name) + self._canon_decode_tag(tag_name, self.tags[tag_name].values, tag_def) + del self.tags[tag_name] + ccitn = makernote.canon.CAMERA_INFO_TAG_NAME + if tag := self.tags.get(ccitn): logger.debug('Canon CameraInfo') - self._canon_decode_camera_info(tag) - del self.tags[makernote.canon.CAMERA_INFO_TAG_NAME] + if self._canon_decode_camera_info(tag): + del self.tags[ccitn] + 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): + def _canon_decode_tag(self, tag_name, 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]) - + for i, val in enumerate(value): + if not i: + # skip id 0 + continue + name, *enum = mn_tags.get(i + 1, (f'Unknown {i+1:3d}', )) + if enum: + val = enum[0].get(val, f'Unknown 0x{val:x}') # 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) + self.tags[f'{tag_name} {name}'] = IfdTag(str(val), 0, 0, val, 0, 0) def _canon_decode_camera_info(self, camera_info_tag): """ @@ -591,7 +591,8 @@ def _canon_decode_camera_info(self, camera_info_tag): 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) + self.tags[f'MakerNote CanonCameraInfo {tag_name}'] = IfdTag(str(tag_value), 0, 0, tag_value, 0, 0) + return True def parse_xmp(self, xmp_bytes: bytes): """Adobe's Extensible Metadata Platform, just dump the pretty XML.""" diff --git a/exifread/heic.py b/exifread/heic.py index 16a581c..f61ea04 100644 --- a/exifread/heic.py +++ b/exifread/heic.py @@ -192,11 +192,12 @@ 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: + + try: + psub = self.get_parser(box) psub(box) meta.subs[box.name] = box - else: + except NoParser as e: logger.debug('HEIC: skipping %r', box) # skip any unparsed data self.skip(box) diff --git a/exifread/tags/makernote/canon.py b/exifread/tags/makernote/canon.py index f41c84c..edbb5f5 100644 --- a/exifread/tags/makernote/canon.py +++ b/exifread/tags/makernote/canon.py @@ -5,13 +5,17 @@ """ TAGS = { + 0x0001: ('CameraSettings',), # see CAMERA_SETTINGS + 0x0002: ('FocalLength',), # see FOCAL_LENGTH 0x0003: ('FlashInfo',), + 0x0004: ('ShotInfo',), # see SHOT_INFO 0x0006: ('ImageType', ), 0x0007: ('FirmwareVersion', ), 0x0008: ('ImageNumber', ), 0x0009: ('OwnerName', ), 0x000c: ('SerialNumber', ), 0x000e: ('FileLength', ), + 0x000d: ('CanonCameraInfo', ), # see 0x0010: ('ModelID', { 0x1010000: 'PowerShot A30', 0x1040000: 'PowerShot S300 / Digital IXUS 300 / IXY Digital 300', @@ -161,8 +165,8 @@ 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', + 0x3130000: 'PowerShot SX40 HS', 0x3160000: 'PowerShot A1300', 0x3170000: 'PowerShot A810', 0x3180000: 'PowerShot ELPH 320 HS / IXUS 240 HS / IXY 420F', @@ -220,7 +224,9 @@ 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', @@ -250,6 +256,7 @@ 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', @@ -293,13 +300,15 @@ 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', + 0x80000349: 'EOS 5D Mark IV', + 0x80000355: 'EOS M2', 0x80000382: 'EOS 5DS', 0x80000393: 'EOS Rebel T6i / 750D / Kiss X8i', 0x80000401: 'EOS 5DS R', }), + 0x0012: ('AFInfo', ), 0x0013: ('ThumbnailImageValidArea', ), 0x0015: ('SerialNumberFormat', { 0x90000000: 'Format 1', @@ -316,16 +325,21 @@ 2: 'Date & Time', }), 0x001e: ('FirmwareRevision', ), + 0x0026: ('AFInfo2', ), # see AF_INFO_2 0x0028: ('ImageUniqueID', ), + 0x0035: ('TimeInfo', ), + 0x0093: ('FileInfo', ), # see FILE_INFO 0x0095: ('LensModel', ), - 0x0096: ('InternalSerialNumber ', ), - 0x0097: ('DustRemovalData ', ), - 0x0098: ('CropInfo ', ), + 0x0096: ('InternalSerialNumber', ), + 0x0097: ('DustRemovalData', ), + 0x0098: ('CropInfo', ), 0x009a: ('AspectInfo', ), 0x00b4: ('ColorSpace', { 1: 'sRGB', 2: 'Adobe RGB' }), + 0x4019: ('LensInfo', ), + } # this is in element offset, name, optional value dictionary format @@ -523,12 +537,27 @@ SHOT_INFO = { 7: ('WhiteBalance', { 0: 'Auto', - 1: 'Sunny', + 1: 'Daylight', 2: 'Cloudy', 3: 'Tungsten', 4: 'Fluorescent', 5: 'Flash', - 6: 'Custom' + 6: 'Custom', + 7: 'Black & White', + 8: 'Shade', + 9: 'Manual Temperature (Kelvin)', + 10: 'PC Set 1', + 11: 'PC Set 2', + 12: 'PC Set 3', + 14: 'Daylight Fluorescent', + 15: 'Custom 1', + 16: 'Custom 2', + 17: 'Underwater', + 18: 'Custom 3', + 19: 'Custom 4', + 20: 'PC Set 4', + 21: 'PC Set 5', + 23: 'Auto (ambience priority)', }), 8: ('SlowShutter', { -1: 'n/a', @@ -563,7 +592,7 @@ # 0x0026 AF_INFO_2 = { - 2: ('AFAreaMode', { + 1: ('AFAreaMode', { 0: 'Off (Manual Focus)', 2: 'Single-point AF', 4: 'Multi-point AF or AI AF', @@ -575,15 +604,26 @@ 11: 'Flexizone Multi', 13: 'Flexizone Single', }), - 3: ('NumAFPoints', ), - 4: ('ValidAFPoints', ), - 5: ('CanonImageWidth', ), + 2: ('NumAFPoints', ), + 3: ('ValidAFPoints', ), + 4: ('CanonImageWidth', ), + 5: ('CanonImageHeight', ), + 6: ('AFImageWidth', ), + 7: ('AFImageHeight', ), + 8: ('AFAreaWidths', ), + 9: ('AFAreaHeights', ), + 10: ('AFAreaXPositions', ), + 11: ('AFAreaYPositions', ), + 12: ('AFPointsInFocus', ), + 13: ('AFPointsSelected', ), + 14: ('PrimaryAFPoint', ), } +AF_INFO_2 = {k+1: v for k, v in AF_INFO_2.items()} # 0x0093 FILE_INFO = { 1: ('FileNumber', ), - 3: ('BracketMode', { + 2: ('BracketMode', { 0: 'Off', 1: 'AEB', 2: 'FEB', @@ -674,7 +714,7 @@ def convert_temp(value): # 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_TAG_NAME = 'MakerNote CanonCameraInfo' CAMERA_INFO_5D = { 23: ('CameraTemperature', ' str: @@ -46,7 +47,7 @@ def ev_bias(seq) -> str: if i == 0: ret_str += 'EV' else: - ratio = Ratio(i, step) + ratio = Fraction(i, step) ret_str = ret_str + str(ratio) + ' EV' return ret_str diff --git a/exifread/utils.py b/exifread/utils.py index aea8b19..d7a0f01 100644 --- a/exifread/utils.py +++ b/exifread/utils.py @@ -79,34 +79,3 @@ def get_gps_coords(tags: dict) -> tuple: 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)