Finally got my first cut at this new plugin/add-on and while I sort out more details on how to make it publicly available on github
Currently it is set for Gramps 5.1 and in my case resides in - on my Mint box
~/user/.gramps/gramps51/plugins/displayMetadata
This add-on depends on having Exiftool installed and to be on the path as well as PyExiftool from
https://smarnach.github.io/pyexiftool/
Off hand I don’t recall whether PyExiftool also installs Exiftool’ linux executable or not.
The add-on does not use GExiv2 at all.
Let me know if there are issues or question and I’ll try my best to sort things out.
++++++++++++++++++++++++++
displayMetadata.gpr.py
++++++++++++++++++++++++++
# File: displayMetadata.gpr.py
register(GRAMPLET,
id="Display Metadata Gramplet",
name=_("Display Metadata Gramplet"),
description = _("Gramplet to display image metadata"),
status = STABLE,
version="0.0.2",
fname="displayMetadata.py",
height = 20,
gramplet = 'DisplayMetadata',
gramplet_title = _("Display Metadata"),
gramps_target_version="5.1",
# help_url="5.2_Addons#Addon_List"
)
++++++++++++++++++++++++++
displayMetadata.py
++++++++++++++++++++++++++
# -*- coding: utf-8 -*-
#!/usr/bin/env python
# DisplayMetadata module
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2009-2011 Rob G. Healey <robhealey1@gmail.com>
# 2019 Paul Culley <paulr2787@gmail.com>
# 2022 Arnold Wiegert nscg111@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#-------------------------------------------------------------------------
#
# GNOME modules
#
#-------------------------------------------------------------------------
from gi.repository import Gtk
import exiftool # pip install pyexiftool
# *****************************************************************************
# Python Modules
# *****************************************************************************
import os
"""
Display Metadata Gramplet
"""
#-------------------------------------------------------------------------
#
# Gramps modules
#
#-------------------------------------------------------------------------
from gramps.gen.plug import Gramplet
from gramps.gen.utils.file import media_path_full
#-------------------------------------------------------------------------
#
# Gramps modules
#
#-------------------------------------------------------------------------
from gramps.gui.listmodel import ListModel
from gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
from gramps.gen.utils.place import conv_lat_lon
from fractions import Fraction
from gramps.gen.lib import Date
from gramps.gen.datehandler import displayer
from datetime import datetime
# ----------------------------------------------------------
class DisplayMetadata(Gramplet):
"""
Displays the metadata of an image.
"""
def init(self):
self.set_text( "Metadata" )
self.gui.WIDGET = self.build_gui()
self.gui.get_container_widget().remove(self.gui.textview)
self.gui.get_container_widget().add(self.gui.WIDGET)
self.gui.WIDGET.show()
# ----------------------------------------------------------
def db_changed(self):
self.connect_signal('Media', self.update)
# ----------------------------------------------------------
def build_gui(self):
"""
Build the GUI interface.
"""
self.view = MetadataView2()
return self.view
# ----------------------------------------------------------
def main(self):
active_handle = self.get_active('Media')
if active_handle:
media = self.dbstate.db.get_media_from_handle(active_handle)
if media:
full_path = media_path_full(self.dbstate.db, media.get_path())
has_data = self.view.display_metadata(full_path)
self.set_has_data(has_data)
else:
self.set_has_data(False)
else:
self.set_has_data(False)
# ----------------------------------------------------------
def update_has_data(self):
active_handle = self.get_active('Media')
if active_handle:
active = self.dbstate.db.get_media_from_handle(active_handle)
self.set_has_data(self.get_has_data(active))
else:
self.set_has_data(False)
# ----------------------------------------------------------
def get_has_data(self, media):
"""
Return True if the gramplet has data, else return False.
"""
if media is None:
return False
full_path = media_path_full(self.dbstate.db, media.get_path())
return self.view.get_has_data(full_path)
# ----------------------------------------------------------
def format_datetime(datestring):
"""
Convert an exif timestamp into a string for display, using the
standard Gramps date format.
"""
try:
timestamp = datetime.strptime(datestring, '%Y:%m:%d %H:%M:%S')
except ValueError:
return _('Invalid format')
date_part = Date()
date_part.set_yr_mon_day(timestamp.year, timestamp.month, timestamp.day)
date_str = displayer.display(date_part)
time_str = _('%(hr)02d:%(min)02d:%(sec)02d') % {'hr': timestamp.hour,
'min': timestamp.minute,
'sec': timestamp.second}
return _('%(date)s %(time)s') % {'date': date_str, 'time': time_str}
# ----------------------------------------------------------
def format_gps(raw_dms, nsew):
"""
Convert raw degrees, minutes, seconds and a direction
reference into a string for display.
"""
value = 0.0
divisor = 1.0
for val in raw_dms.split(' '):
try:
num = float(val.split('/')[0]) / float(val.split('/')[1])
except (ValueError, IndexError):
value = None
break
value += num / divisor
divisor *= 60
if nsew == 'N':
result = conv_lat_lon(str(value), '0', 'DEG')[0]
elif nsew == 'S':
result = conv_lat_lon('-' + str(value), '0', 'DEG')[0]
elif nsew == 'E':
result = conv_lat_lon('0', str(value), 'DEG')[1]
elif nsew == 'W':
result = conv_lat_lon('0', '-' + str(value), 'DEG')[1]
else:
result = None
return result if result is not None else _('Invalid format')
# ----------------------------------------------------------
class MetadataView2(Gtk.TreeView):
def __init__(self):
Gtk.TreeView.__init__(self)
self.sections = {}
titles = [(_('Key'), 1, 235),
(_('Value'), 2, 325)]
self.model = ListModel(self, titles, list_mode="tree")
# ----------------------------------------------------------
def display_metadata(self, full_path):
"""
Display the metadata
"""
self.sections = {}
self.model.clear()
if not os.path.exists(full_path):
head, tail = os.path.split( full_path )
label = tail
node = self.__add_section('File not found')
label = tail
human_value = head
self.model.add((label, human_value), node=node)
return False
retval = False
n = 0
with open(full_path, 'rb') as fd:
with exiftool.ExifToolHelper() as et:
for d in et.get_metadata(full_path):
# first set of lines
# Dict: SourceFile = D:/test.jpg
# Dict: ExifTool:ExifToolVersion = 12.44
# Dict: File:FileName = test.jpg
#
# k -> composite tag label
# Exiftool 'group' a prefix with ':' separator
# such as:
# IPTC:Caption-Abstract = Wilting Rose
# v -> all values converted to a string
#needHeader = False
#header = ""
lastLeadin = ""
leadin = "exiftool:"
for k, v in d.items():
if False: #gl.globalDebug:
print(f"Dict: {k} = {v}") # goes to terminal
name = str(k)
val = str(v)
# Note only Sourcefile has a simple name & no ':'
# for this entry the 2nd part is never used
leadin = name.split(':',2)
start = leadin[0]
#if gl.globalDebug:
#if int(gl.globalLogVerbosity) >= 2:
#self.m_textCtrlLog.AppendText( name +" = " + val + "\n" )
if lastLeadin != start:
#if gl.globalDebug:
#if int(gl.globalLogVerbosity) >= 2:
#self.m_textCtrlLog.AppendText( " start: " + start +" lastLeadin: " + lastLeadin + "\n" )
print( " start: " + start +" lastLeadin: " + lastLeadin + "\n" )
lastLeadin = start
#needHeader = True
#header = start
if name.startswith("SourceFile"):
# skip this line, the data is displayed in the status bar
continue
cleanName = leadin[1]
# See the IPTC Spec IIMV4.1.pdf
if cleanName == "CodedCharacterSet":
if val == "\x1b%G":
val = 'UTF8'
# use the fully qualified name for the 'All' grid to avoid issues
# when the second part of the name occurs repeatedly but in a different 'context',
# such as
# JFIF:XResolution = 72 and
# EXIF:XResolution = 72
# if needHeader:
# needHeader = False
node = self.__add_section(start)
label = cleanName #name
human_value = val
self.model.add((label, human_value), node=node)
self.model.tree.expand_all()
if self.model.count == 0:
head, tail = os.path.split( full_path )
label = tail
node = self.__add_section('No Metadata found')
label = tail
human_value = 'No metadata'
self.model.add((label, human_value), node=node)
retval = self.model.count > 0
return retval
# ----------------------------------------------------------
def __add_section(self, section):
"""
Add the section heading node to the model.
"""
if section not in self.sections:
node = self.model.add([section, ''])
self.sections[section] = node
else:
node = self.sections[section]
return node
# ----------------------------------------------------------
def get_has_data(self, full_path):
"""
Return True if the gramplet has data, else return False.
"""
if not os.path.exists(full_path):
return False
with open(full_path, 'rb') as fd:
retval = False
try:
buf = fd.read()
metadata = GExiv2.Metadata()
metadata.open_buf(buf)
for tag in TAGS:
if tag in metadata.get_exif_tags():
retval = True
break
except:
pass
return retval
# ---------------------------- eof ------------------------------