0012219: Expand ImageMetadata gramplet to support XMP and IPTC metadata tags

@ ebj075
Thank you for giving some more examples to study and help me along the way.

For more than a decade, I have been looking at metadata in images for my family tree images and have not really found a good way to address this issue.
All of this has lead me to dig into the innards of Gramps and Python.
As far as the current ‘Image Metadata’ gramplet goes, I’ll leave that alone since I think it has some serious drawback as, I’m afraid, also does your vast improvement over the original.
I am merely using both as examples to get me going with my own version with different design goals.

My main concern with the current code, is that:

a) EXIF is not really where most of the interesting data resides and your version has started to tackle that issue by adding other tags which are way more relevant for genealogy than those in EXIF.

b) Until there is some sort of agreement among Gramps users and genealogy users in general as to which tags are of interest for this work, it is not only way too labor intensive to have all of the tags available to the user hard wired in the code. It is also very much in danger of becoming outdated very quickly.
I know of FHMWG but nothing seems to have come out of that for quite some time …

My current design goal is to simply list all of the metadata that Gexiv2 can find in an image.
There are plenty of WIBNIs (Wouldn’t It Be Nice If ) in my mind, but there is just so much time am so much to learn about Python, GTK, gi, GExiv2 etc

My current progress has been to make a copy of the existing code and turn it into a third-party add-on which for now lives in my user directory, without the need to major surgery in the bowels of Gramps.
The only current minor improvements is to let the user know when no metadata is found in either of the major sections EXIF, XMP IPTC.

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 ------------------------------
1 Like

That looks good! I did have to follow the instructions at the exiftool website to build and install exiftool in my WSL instance. It did not work with exiftool that had been installed with apt.

Good to hear; I had installed Exiftool and its Python interface some time ago and so the specifics were rather hazy.

Does your work in this area enable a fix for https://gramps-project.org/bugs/view.php?id=12480?

David Lynch

No idea, but …
While the add-on works in my Mint box, it does not show any output when I install it in my Windows PC.

Therefor one of the next jobs is to figure out how to get a closer look at why not.
Either I have to figure out how to get Gramps to log the details under Windows or build up a development system system under Windows - presumably using MSYS - and figure out if that would allow me to debug under Windows or if it simply meant to allow building the AIO packages or
find a way to debug the Python code under Windows in an already installed copy of Gramps - and suitably isolated.

I have tried the portable version at one time, but aside from isolating and protecting my data, I see no advantage to that and beside, IIRC, it is so slow, I decide to never use that way again. :slight_smile:

Found out how to run Gramps under Windows with logging to the DOS Window:
C:\Program Files\GrampsAIO64-5.1.5>gramps --debug=

My new add-on does nothing because I have not installed the necessary PyExiftool
Finding out how and installing it .will be the next job

Under Windoze 10 home, clicking on a JPG with the Display Metadata Gramplet active in the Media sidebar adds a warning icon to the statusbar.

Clicking the warning displays the warning dialog with:

WARNING ._manager: Plugin error (from 'displayMetadata'):  no module named 'exiftool'
WARNING .: Error loading Gramplet 'Display Metadata Gramplet': skipping content 

Could you look into what required to have exiftool properly registered so that the Prerequisites Checker Gramplet add-on will report it properly?

And maybe have a fallback when the prerequisite is missing that make the Gramplet indicate a failure rather than just have a blank window?

Yes, according to my previous reply, that is the next job on my to-do list.
As I am very new to all of this, my big problem is to wrap my head around all of the things that are different under Windows.
Right now, installing or making Exiftool available to a Windows AIO bundle is the job, since I know I do have Exiftool installed on this PC.

FWIW, the current Windows 10 version have hidden the ‘Open DOS’ window from the context menu, but one can use the Powershell window, if one uses a more specific command line, including the path, such as:
.\gramps.exe --debug=

1 Like

Thank you for pointing out the warning icon; I had never noticed it before - just new to that part of Gramps.

As for the prerequisites tool , it looks like each add-on to be checked needs to be added to the gramplet code, so it does not seem too useful for that purpose.

From what I know right now, I see no way to actually check whether the requirements for my new add-on are met, because Gramps bombs out long before any of the add-on code is executes. Hence I see no way to even let the user know about this.

My second problem with the Windows version is that I have not figured out how I can install a new requirement for an existing installation, which is all that happens when one adds the code in the user add-on directory.

After spending a lot of time, without any success, trying to make the code suggested to have Gramps actually install the necessary packages for my displayMetadata & pyExiftool requisites, I have given up.
This was prompted in large part after coming across the comments in Where is the Gramps distribution of Python on Windows? - #5 by PLegoux and more specifically by the comments in
0010913: Face detection in Photo Tagging Gramplet (Does NOT work in the[Microsoft Windows] AIO bundle(s)) - Gramps - Bugtracker – Free Genealogy Software

As it turned out, I realized that displayMetadata does in fact also need numpy.
There is just no way I can take on the effort to even build Gramps from the ground up as suggested in the first link
https://www.gramps-project.org/wiki/index.php/Gramps_for_Windows_with_MSYS2

As well, at this time I don’t see that approach being workable for me, if only because, if I want to the plugin to be usable by others, they also would have to go the same direction. Of course, the Photo Tagging gramplet also looks attractive - more work.
While I am still weighing my options, going forward with a Linux only solution looks simpler than trying to keep ‘working around’ the Win/Lin differences.

Try using the GExiv2 package instead of pyExiftool. GExiv2 is available as a MYSYS2 package.

I have thought about GExiv2 and realize the older gramplet I am trying to replace uses that package.
But, using it would not help me much, for several reasons.

For one, in the past, I had used the exiv2 package, on which GEviv2 is based, but IMO, updating and rebuilding GExiv2 is as much trouble as going MSYS all the way.
As well, it is my impression that GExiv2 is somewhat behind the base Exiv2 package whose main maintainer has recently retired and I have not kept up with the current status. Either way, I understand that with either package I am depending on an ‘outside’ resource with all that goes with that

Another problem would be that if I want to use any other plugin, such as the “Photo Tagging”, even for testing, I would still be faced with the MSYS solution it seems.

Down the road, I might consider these alternatives, but for the time being they would take - and already have taken me - far further off track than I think worthwhile for now.

Our Windows and Mac bundles already include the GExiv2 package.

Understood, but I am not sure how up-to-date that package is.
How would I find out and what would be involved to keep it up-to-date?

The current versions are exiv2 0.27.5 and gexiv2 0.14.0.

1 Like

If the Gevi2 version numbers are in any way related to Exiv2 version, GExiv2 is way behind and would need an update.

As it is, the existing plugin’s display of only EXIF (basically related to the hardware - i.e. camera) is not all that relevant to the metadata of most interest for genealogy purposes. That data is typically found in IPTC and XMP data.

When I last looked at the plugin some time ago, it raised a question: is there a way for plugins to have a config file. IIRC, the current plugin has the data to be displayed baked into the code itself.

The two version numbers are unrelated.

Pull request #1263 adds support for XMP and IPTC tags. It is easy to add configuration options to gramplets. I can show you how to do this if you are interested in working with the author of the PR.

If the two version numbers are unrelated, how can I find out which Exiv2 version the current GExiv2 code is based on?
My current view is that Exiftool is well enough maintained by a single developer and for the time being, I would much prefer sticking with that option.
Still, for anyone wanting to use GExiv2 or any other package if they so choose, that option is available. It is one of the main reasons why I decided to not just modify the existing plugin.

If using support (including updates) for GExiv2 means I have to install MSYS on my system (and become familiar enough with it), I find it hard to convince myself to not simply go ‘whole hog’ and install a Linux VM system on my Windows machine. In the end, that is all MSYS provides, is it not?

An even better solution will be to use a web based-design, but that is not something I can spend time on.

On the whole, though, and considering some of the details about the AOI package I have found recently, my big concern whether it is reasonable to work with or on the AOI package at all.

FWIW, I have, in the past, years ago, worked with and on the AOI package, building my local copy for a while.
Though that code and experience is somewhere in the rearview mirror and dust of the past and mostly lost and left on an old and decommissioned PC.

Still, and I am not intending to offend anyone, my view of AOI’s future has been severely dented when I came to realize the limitations of the AOI package while I investigated adding my plugin to it.
As I see it now, the AOI package really is just another VM environment, but with a good number of limitations, and what’s more the Gramps AOI maintainers have the task of maintaining it, as well as Gramps. Why not go to a Gramps VM appliance, based on Linux?

As for PR #1263, I have not had the time to get more familiar with it, though it certainly will be of interest as will the issue of adding configuration options. IMO, they are essential, to allow other users handle their need in their own way.

But, my time and resources are limited and I will need to focus on getting the work done without spreading myself too thin. Hence I will concentrate on Linux only, possibly also in a VM on Windows.

Our Windows AIO includes exiv2 0.27.5 and our Mac bundle includes exiv2 0.27.4. Both use version 0.14.0 of the gexiv2 python bindings.

You don’t have to use MSYS2 in order to use GExiv2.