#!/usr/bin/python # -*- coding: utf8 -*- # # Add Exif metadata over photos taken with a Canon S120. # Fix missing or misleading tags as reported by Exiv2 v.0.24. # # Author: Niccolo Rigacci # Version: 0.2 2019-08-23 # http://www.exiv2.org/metadata.html # http://dev.exiv2.org/projects/exiv2/repository/entry/trunk/src/canonmn.cpp # http://www.burren.cx/david/canon.html # http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Canon.html import collections import os import os.path import subprocess import sys # Just print metadata, don't make an overlay image. JUST_PRINT = True # Prepend filename before tag names. PREPEND_FILENAME = False #------------------------------------------------------------------------- # Missing or incorrect values from Exiv2 library v.0.24 #------------------------------------------------------------------------- # Missing Exif.CanonCs.ISOSpeed tag 0x10 values. canonCsISOSpeed = { '(16509)': u'125', '(16634)': u'250', '(16704)': u'320', '(16884)': u'500', '(17024)': u'640', '(17384)': u'1000', '(17634)': u'1250', '(18384)': u'2000', '(18884)': u'2500', '(20384)': u'4000', '(21384)': u'5000', '(22784)': u'6400', '(24384)': u'8000', '(26384)': u'10000', '(29184)': u'12800' } # Missing/misleading Exif.CanonCs.AFPoint tag 0x13 values. canonCsAfPoint = { '(0)': u'None', '(16390)': u'Face AiAF', 'Manual AF point selection': u'1-point' } #------------------------------------------------------------------------- # Tags to be printed (as shown by "exiv print -p a") and label. #------------------------------------------------------------------------- exif_tag = collections.OrderedDict() # Camera modes exif_tag['Exif.CanonCs.ExposureProgram'] = u'Program' exif_tag['Exif.CanonCs.FocusMode'] = u'Focus mode' # Single, Manual focus exif_tag['Exif.CanonCs.FocusType'] = u'Focus type' # Auto, Macro, Manual #exif_tag['Exif.CanonCs.FocusContinuous'] = u'Continuous focus' exif_tag['Exif.CanonCs.AFPoint'] = u'Auto-Focus frame' #exif_tag['Exif.CanonSi.AFPointUsed'] = u'Auto-Focus points used' # Always "0 focus points; none used" exif_tag['Exif.CanonCs.MeteringMode'] = u'Canon Metering mode' # Evaluative, Center weighted, Spot, ... exif_tag['Exif.CanonCs.AESetting'] = u'Auto-Exposure mode' # Normal AE, ... exif_tag['Exif.Canon.0x0027'] = u'Exif.Canon.0x0027' exif_tag['Exif.Canon.0x0027.16'] = u'Dynamic Range Correction' exif_tag['Exif.Canon.0x0027.4'] = u'Shadow Correct' #exif_tag['Exif.CanonCs.FlashMode'] = u'Flash mode' exif_tag['Exif.Photo.MeteringMode'] = u'Metering mode' # Multi-segment, Center weighted average, Spot ,... exif_tag['Exif.Photo.ExposureMode'] = u'Exposure mode' # Auto, Manual, Auto bracket exif_tag['Exif.Photo.WhiteBalance'] = u'White balance' exif_tag['Exif.CanonCs.ISOSpeed'] = u'ISO mode' #exif_tag['Exif.CanonSi.ISOSpeed'] = u'ISO speed selected' # Very close to Exif.Photo.ISOSpeedRatings. It is 200 when ISO mode is Auto. exif_tag['Exif.Photo.ExposureBiasValue'] = u'Exposure bias' exif_tag['Exif.CanonSi.MeasuredEV'] = u'Measured EV' #exif_tag['Exif.CanonSi.MeasuredEV2'] = u'Measured EV2' # Always -6.0 ? exif_tag['sep1'] = None exif_tag['Exif.CanonSi.SubjectDistance'] = u'Subject distance' # Aperture exif_tag['Exif.Photo.FNumber'] = u'Aperture f-number' #exif_tag['Exif.Photo.ApertureValue'] = u'Lens APEX aperture' # Always equal to Exif.Photo.FNumber #exif_tag['Exif.CanonSi.TargetAperture'] = u'Target Aperture' # Sometimes differ for few decimal points from Exif.CanonSi.ApertureValue #exif_tag['Exif.CanonSi.ApertureValue'] = u'Aperture' # Sometimes differ for few decimal points from Exif.Photo.FNumber # Exposure time exif_tag['Exif.Photo.ExposureTime'] = u'Exposure time' #exif_tag['Exif.Photo.ShutterSpeedValue'] = u'APEX shutter speed' # Differs slightly from Exif.Photo.ExposureTime #exif_tag['Exif.CanonSi.TargetShutterSpeed'] = u'Target shutter speed' # Always equal to Exif.Photo.ShutterSpeedValue #exif_tag['Exif.CanonSi.ShutterSpeedValue'] = u'Shutter speed Canon' # Differs slightly from above values exif_tag['Exif.Photo.ISOSpeedRatings'] = u'ISO speed' exif_tag['Exif.Photo.FocalLength'] = u'Focal length' exif_tag['Exif.Photo.Flash'] = u'Flash' #------------------------------------------------------------------------- # Make an array of printable Exif tags and stamp it over the photo. #------------------------------------------------------------------------- def make_exif_overlay_image(photo): if not os.path.isfile(photo): print(u"Error: File \"%s\": not found" % (photo.encode('utf-8'),)) return photo_exif = photo[0:-4] + '.n.exif.jpg' found_tag = {} orientation = None cmd = ['exiv2', 'print', '-u', '-p', 'a', photo] subproc = subprocess.Popen(cmd, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, stderr = subproc.communicate() retcode = subproc.returncode for line in output.splitlines(): field = line.split(None, 3) if field[0] in exif_tag: found_tag[field[0]] = field[3].decode('utf-8') if field[0] == 'Exif.Image.Orientation': # Get image orientation. orientation = field[3].decode('utf-8') elif field[0] == 'Exif.CanonCs.ISOSpeed': # Some Exif.CanonCS.ISOSpeed are missing in Exiv2 library. if field[3] in canonCsISOSpeed: found_tag[field[0]] = canonCsISOSpeed[field[3]] elif field[0] == 'Exif.CanonSi.SubjectDistance': # Convert distance in a fancy number. distance = int(field[3]) if distance == 6553: found_tag[field[0]] = u'Infinity (∞)' elif distance < 100: found_tag[field[0]] = u'%d cm' % (distance) else: found_tag[field[0]] = u'%.2f m' % (float(distance) / 100.0) elif field[0] == 'Exif.CanonCs.AFPoint': # Fix missing/misleading values for Exif.CanonCs.AFPoint. if field[3] in canonCsAfPoint: found_tag[field[0]] = canonCsAfPoint[field[3]] elif field[0] == 'Exif.Canon.0x0027': #del found_tag['Exif.Canon.0x0027'] subfield = field[3].split() # Byte 16: Dynamic Range Correction. found_tag['Exif.Canon.0x0027.16'] = u"{0:08b} ".format(int(subfield[4])) if subfield[16] == '0': found_tag['Exif.Canon.0x0027.16'] += u'Off' elif subfield[16] == '1': found_tag['Exif.Canon.0x0027.16'] += u'Auto' elif subfield[16] == '200': found_tag['Exif.Canon.0x0027.16'] += u'200%' elif subfield[16] == '400': found_tag['Exif.Canon.0x0027.16'] += u'400%' else: found_tag['Exif.Canon.0x0027.16'] += u'(%s)' % (subfield[16]) # Byte 4: Dynamic Range Correction and Shadow Correct # # Bit 0: Shadow Correct setting: 0 = Off, 1 = Auto # Bit 3: i-Contrast (Dynamic Range Correction or Shadow Correct): 0 = no, 1 = applied ? # Bit 4: Shadow Correct applied: 0 = No, 1 = Yes # # Camera will display the icon "Shadow Correct" only for flags = 0b10001 (Yes + Auto), # but some photos have flags = 0b10000 (Yes + Off) Shadow Correct set by Auto program? if int(subfield[4]) & 0b00001: found_tag['Exif.Canon.0x0027.4'] = u'Auto' else: found_tag['Exif.Canon.0x0027.4'] = u'Off' if int(subfield[4]) & 0b10000: found_tag['Exif.Canon.0x0027.4'] += u' (Yes)' else: found_tag['Exif.Canon.0x0027.4'] += u' (No)' # Sometimes this flag is ON, but DR subfield[16] = 0; DR just evaluated? #if int(subfield[4]) & 0b01000: # found_tag['Exif.Canon.0x0027.16'] += ' (DR flag ON)' # # TODO: temporary debug # found_tag['Exif.Canon.0x0027.4'] += ' (DR flag ON)' ## TODO: temporary debug #found_tag['Exif.Canon.0x0027.16'] += " - Shadow Correct: " + found_tag['Exif.Canon.0x0027.4'] overlay = [] for tag in exif_tag: if tag in found_tag: if PREPEND_FILENAME: overlay.append("%s:%s: %s" % (photo, exif_tag[tag], found_tag[tag])) else: overlay.append(u"%s: %s" % (exif_tag[tag], found_tag[tag])) elif tag.startswith("sep"): overlay.append("") overlay_text = "\n".join(overlay) if JUST_PRINT: print(u"===== %s =====" % (os.path.basename(photo).encode('utf-8'),)) print(overlay_text.encode('utf-8')) print("") return # Overlay size and rotation. if orientation == u'right, top': rotate = '270' size_format = '%hx%w' elif orientation == u'left, bottom': rotate = '90' size_format = '%hx%w' else: # Normal and upside-down position is always "top, left". rotate = '0' size_format = '%wx%h' # Get image size (e.g. size = '4000x3000') and font pointsize. cmd = ['identify', '-format', size_format, photo] size = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0] pointsize = str(int(size.split("x")[0]) / 42) cmd = [ 'convert', '-size', size, 'xc:none', '-font', 'Trebuchet-MS-Bold', '-pointsize', pointsize, '-gravity', 'NorthWest', '-stroke', 'black', '-strokewidth', '30', '-annotate', '+20+20', overlay_text, '-blur', '0x15', '-stroke', 'none', '-fill', 'yellow', '-annotate', '+20+20', overlay_text, '-rotate', rotate, photo, '+swap', '-gravity', 'South', '-geometry', '+0-0', '-composite', photo_exif ] if not os.path.isfile(photo_exif): subprocess.call(cmd) #------------------------------------------------------------------------- # Main loop: iterate on all arguments. #------------------------------------------------------------------------- for i in range(1, len(sys.argv)): if sys.argv[i] == '-j': JUST_PRINT = False continue make_exif_overlay_image(sys.argv[i])