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

HEIC + iPhone: ignore heic boxes #171

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion exifread/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
67 changes: 34 additions & 33 deletions exifread/classes.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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 = ''
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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."""
Expand Down
7 changes: 4 additions & 3 deletions exifread/heic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
67 changes: 54 additions & 13 deletions exifread/tags/makernote/canon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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', '<B', convert_temp),
Expand Down Expand Up @@ -708,4 +748,5 @@ def convert_temp(value):
r'EOS 5D Mark II$': CAMERA_INFO_5DMKII,
r'EOS 5D Mark III$': CAMERA_INFO_5DMKIII,
r'\b(600D|REBEL T3i|Kiss X5)\b': CAMERA_INFO_600D,
r'EOS 5D Mark IV$': CAMERA_INFO_5DMKIII,
}
5 changes: 3 additions & 2 deletions exifread/tags/makernote/nikon.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fractions import Fraction

from exifread.utils import make_string, Ratio
from exifread.utils import make_string


def ev_bias(seq) -> str:
Expand Down Expand Up @@ -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

Expand Down
31 changes: 0 additions & 31 deletions exifread/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)