Sunday 6 March 2016

Custom EOG rating plugin

When using a low powered netbook as a travel companion running Linux, the Gnome EOG image viewer can be used to provide an initial review tool, with a little hacking.

Gnome/GTK can be enhanced to understand Nikon RAW files and the EOG viewer can also be enhanced to rate images (by modifying the EXIF/XMP tags) whilst on the move.

Notebooks and their low processing abilities means that they can not be expected to process large RAW files so reviews can be done on embedded preview images via EOG. With the EOG extension framework, a custom plugin can be written to rate the Nikon RAW image from within EOG. When you return to base, the rated Nikon RAW files can then be viewed in CaptureNX2 and the rated files can be identified for proper processing.

The plugin below will use the underlying Exiv2 library to update the Nikon RAW files, inserting or removing an XMP rating tag for the file - pressing 'R' will rate or unrate the file.

Github repo source files

The plugin code:
#ifndef __EOG_EXIV_XMP_RATING_PLUGIN_H__
#define __EOG_EXIV_XMP_RATING_PLUGIN_H__

extern "C" {

#include <glib.h>
#include <glib-object.h>
#include <libpeas/peas-extension-base.h>
#include <libpeas/peas-object-module.h>

#include <eog/eog-window.h>

G_BEGIN_DECLS

/*
* Type checking and casting macros
*/
#define EOG_TYPE_EXIV_XMP_RATING_PLUGIN (eog_exiv2_ratings_plugin_get_type ())
#define EOG_EXIV_XMP_RATING_PLUGIN(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), EOG_TYPE_EXIV_XMP_RATING_PLUGIN, EogExiv2RatingPlugin))
#define EOG_EXIV_XMP_RATING_PLUGIN_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), EOG_TYPE_EXIV_XMP_RATING_PLUGIN, EogExiv2RatingPluginClass))
#define EOG_IS_EXIV_XMP_RATING_PLUGIN(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), EOG_TYPE_EXIV_XMP_RATING_PLUGIN))
#define EOG_IS_EXIV_XMP_RATING_PLUGIN_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), EOG_TYPE_EXIV_XMP_RATING_PLUGIN))
#define EOG_EXIV_XMP_RATING_PLUGIN_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), EOG_TYPE_EXIV_XMP_RATING_PLUGIN, EogExiv2RatingPluginClass))

typedef struct _EogExiv2RatingPluginPrivate EogExiv2RatingPluginPrivate;

typedef struct _EogExiv2RatingPlugin EogExiv2RatingPlugin;

class _ExifProxy;

struct _EogExiv2RatingPlugin
{
PeasExtensionBase parent_instance;

EogWindow *window;
GtkWidget *statusbar_exif;
GtkActionGroup *ui_action_group;
guint ui_id;

_ExifProxy* exifproxy;
};

typedef struct _EogExiv2RatingPluginClass EogExiv2RatingPluginClass;

struct _EogExiv2RatingPluginClass
{
PeasExtensionBaseClass parent_class;
};

GType eog_exiv2_ratings_plugin_get_type (void) G_GNUC_CONST;

G_MODULE_EXPORT void peas_register_types (PeasObjectModule *module);

G_END_DECLS

}
#endif

Implementation
extern "C" {
#include <sys/stat.h>
#include <unistd.h>
#include <utime.h>

extern "C" {

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "eog_plugin_exiv2_ratings.h"

#include <gmodule.h>

#include <libpeas/peas.h>

#include <eog/eog-application.h>
#include <eog/eog-debug.h>
#include <eog/eog-thumb-view.h>
#include <eog/eog-window.h>
#include <eog/eog-window-activatable.h>
}


#include <string>
#include <list>
#include <iostream>
#include <iomanip>
#include <cassert>
#include <cmath>

#include <exiv2/exiv2.hpp>


enum {
PROP_0,
PROP_WINDOW
};

#define EOG_EXIV_XMP_RATING_PLUGIN_MENU_ID "EogPluginRunExiv2Rating"
#define EOG_EXIV_XMP_RATING_PLUGIN_ACTION "exiv2rating"
#define EOG_EXIV_XMP_RATING_PLUGIN_ACTION_U "exiv2rating_u"

static void eog_window_activatable_iface_init (EogWindowActivatableInterface *iface);

G_DEFINE_DYNAMIC_TYPE_EXTENDED (EogExiv2RatingPlugin,
eog_exiv2_ratings_plugin,
PEAS_TYPE_EXTENSION_BASE,
0,
G_IMPLEMENT_INTERFACE_DYNAMIC (EOG_TYPE_WINDOW_ACTIVATABLE,
eog_window_activatable_iface_init))

using namespace std;

static
ostream& operator<<(ostream& os_, const Exiv2::XmpData& data_)
{
for (Exiv2::XmpData::const_iterator md = data_.begin();
md != data_.end(); ++md)
{
#if 0
os_ << std::setfill(' ') << std::left << std::setw(44)
<< md->key() << " "
<< std::setw(9) << std::setfill(' ') << std::left
<< md->typeName() << " " << std::dec << std::setw(3)
<< std::setfill(' ') << std::right
<< md->count() << " " << std::dec << md->
value() << std::endl;
#else
os_ << "{ " << md->key() << " " << md->typeName() << " " << md->count() << " " << md->value() << " }";
#endif
}

return os_;
}


class _ExifProxy
{
public:
friend ostream& operator<<(ostream&, const _ExifProxy&);

struct HistoryEvnt {
HistoryEvnt(const string& f_, const Exiv2::ExifData& exif_, bool rated_)
: f(f_), exif(exif_), rated(rated_)
{ }

HistoryEvnt(const string& f_, const Exiv2::ExifData* exif_ = NULL, bool rated_=false)
: f(f_), exif(exif_ == NULL ? Exiv2::ExifData() : *exif_), rated(rated_)
{ }

const string f;
const Exiv2::ExifData exif;
const bool rated;
};

typedef list<HistoryEvnt> History;


_ExifProxy() : _xmp(NULL), _xmpkpos(NULL), _mtime(0)
{ }

_ExifProxy& ref(const EogThumbView& ev_)
{
return ref(*eog_thumb_view_get_first_selected_image(&ev_));
}

_ExifProxy& ref(const EogWindow& ew_)
{
return ref( *eog_window_get_image(&ew_) );
}

_ExifProxy& ref(const EogImage& ei_)
{
GFile* f = eog_image_get_file(&ei_ );
if ( f == NULL) {
_clear();
return *this;
}

char* const fpath = g_file_get_path(f);
if (strcmp(fpath, _file.c_str()) == 0) {
g_free(fpath);
return *this;
}
struct stat st;
memset(&st, 0, sizeof(st));
if (stat(fpath, &st) < 0) {
_clear();
g_free(fpath);
return *this;
}

_clear();
_mtime = st.st_mtime;

try
{
_img = Exiv2::ImageFactory::open(fpath);
_img->readMetadata();
//_xmpkpos = NULL;

_file = fpath;

Exiv2::XmpData& xmpData = _img->xmpData();
Exiv2::XmpData::iterator kpos = xmpData.findKey(Exiv2::XmpKey(_ExifProxy::_XMPKEY));
if (kpos != xmpData.end()) {
_xmp = &xmpData;
_xmpkpos = kpos;
}
}
catch(Exiv2::AnyError & e) {
cerr << fpath << ": failed to set XMP rating - " << e << endl;
}
g_free(fpath);

return *this;
}

bool valid() const
{
return _img.get() != 0;
}

bool rated() const
{
return _xmp == NULL ? false : _xmpkpos != _xmp->end();
}

/* mark the image if its not already rated
*/
bool fliprating()
{
bool flipped = false;
if (_img.get() == 0) {
// something went wrong earlier/no img ... do nothing
return flipped;
}

try
{
bool r = false;
if (_xmp == NULL)
{
// previously found no XMP data set
_xmp = &_img->xmpData();

(*_xmp)[_ExifProxy::_XMPKEY] = _ExifProxy::_XMPVAL;
_xmpkpos = _xmp->findKey(Exiv2::XmpKey(_ExifProxy::_XMPKEY));

r = true;
}
else
{
if (_xmpkpos == _xmp->end()) {
(*_xmp)[_ExifProxy::_XMPKEY] = _ExifProxy::_XMPVAL;
_xmpkpos = _xmp->findKey(Exiv2::XmpKey(_ExifProxy::_XMPKEY));

r = true;
}
else {
_xmp->erase(_xmpkpos);
_xmpkpos = _xmp->end();
r = false;
}
}
_img->writeMetadata();

flipped = true;

struct utimbuf tmput;
memset(&tmput, 0, sizeof(tmput));
tmput.modtime = _mtime;
utime(_file.c_str(), &tmput);

_ExifProxy::History::iterator h;
for (h=_history.begin(); h!=_history.end(); ++h)
{
if (h->f == _file) {
break;
}
}
if (h != _history.end()) {
_history.erase(h);
}
else
{
_history.push_back(_ExifProxy::HistoryEvnt(_file, _img->exifData(), r));
}
}
catch (const exception& ex)
{
cerr << _file << ": failed to update XMP rating - " << ex.what() << endl;
}
return flipped;
}


const string& file() const
{ return _file; }

const _ExifProxy::History& history() const
{ return _history; }


private:
_ExifProxy(const _ExifProxy&);
void operator=(const _ExifProxy&);

static const string _XMPKEY;
static const Exiv2::XmpTextValue _XMPVAL;


time_t _mtime;

Exiv2::Image::AutoPtr _img;
Exiv2::XmpData* _xmp;
Exiv2::XmpData::iterator _xmpkpos;

string _file;


void _clear()
{
_xmp = NULL;
_file.clear();
_img.reset();
_mtime = 0;
}

_ExifProxy::History _history;
};

const string _ExifProxy::_XMPKEY = "Xmp.xmp.Rating";
const Exiv2::XmpTextValue _ExifProxy::_XMPVAL = Exiv2::XmpTextValue("1");


ostream& operator<<(ostream& os_, const _ExifProxy& obj_)
{
os_ << obj_._file;
if (obj_._xmp == NULL) {
return os_ << " [ <nil> ]";
}
return os_ << " [ " << *obj_._xmp << " ]";
}

ostream& operator<<(ostream& os_, const _ExifProxy::HistoryEvnt& obj_)
{
struct _ETag {
const Exiv2::ExifKey tag;
const char* prfx;

_ETag(const Exiv2::ExifKey& tag_, const char* prfx_) : tag(tag_), prfx(prfx_) { }
};
static const _ETag etags[] = {
_ETag(Exiv2::ExifKey("Exif.Image.Model"), ""),
_ETag(Exiv2::ExifKey("Exif.Image.DateTime"), ""),
_ETag(Exiv2::ExifKey("Exif.Photo.ExposureTime"), ""),
_ETag(Exiv2::ExifKey("Exif.Photo.FNumber"), ""),
_ETag(Exiv2::ExifKey("Exif.Photo.ISOSpeedRatings"), "ISO")
};

os_ << obj_.f << ": " << (obj_.rated ? "R" : "U");

for (int i=0; i<5; ++i) {
Exiv2::ExifData::const_iterator e = obj_.exif.findKey(etags[i].tag);
if (e == obj_.exif.end()) {
continue;
}

os_ << " " << etags[i].prfx << *e;
}

return os_;
}


static void
_upd_statusbar_exif(GtkStatusbar* statusbar_,
const Exiv2::ExifData* exif_, const char* msg_)
{
gtk_statusbar_pop(statusbar_, 0);
if (msg_ == NULL) {
gtk_widget_hide(GTK_WIDGET(statusbar_));
return;
}
gtk_statusbar_push(statusbar_, 0, msg_);
gtk_widget_show(GTK_WIDGET(statusbar_));
}


static void
_exiv2_rating_setunset(GSimpleAction *simple, GVariant *parameter, gpointer user_data, const bool set_)
{
EogExiv2RatingPlugin *plugin = EOG_EXIV_XMP_RATING_PLUGIN (user_data);

_upd_statusbar_exif(GTK_STATUSBAR(plugin->statusbar_exif),
NULL,
plugin->exifproxy->fliprating() ?
(plugin->exifproxy->rated() ? "rated" : "un-rated") :
"-/-");
}

static void
_exiv2_rating_set_cb(GSimpleAction *simple, GVariant *parameter, gpointer user_data)
{
_exiv2_rating_setunset(simple, parameter, user_data, true);
}

static void
_exiv2_rating_unset_cb(GSimpleAction *simple, GVariant *parameter, gpointer user_data)
{
_exiv2_rating_setunset(simple, parameter, user_data, false);
}


static void
eog_exiv2_ratings_plugin_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
EogExiv2RatingPlugin *plugin = EOG_EXIV_XMP_RATING_PLUGIN (object);

switch (prop_id)
{
case PROP_WINDOW:
plugin->window = EOG_WINDOW (g_value_dup_object (value));
break;

default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}

static void
eog_exiv2_ratings_plugin_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
EogExiv2RatingPlugin *plugin = EOG_EXIV_XMP_RATING_PLUGIN (object);

switch (prop_id)
{
case PROP_WINDOW:
g_value_set_object (value, plugin->window);
break;

default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}

static void
eog_exiv2_ratings_plugin_init (EogExiv2RatingPlugin *plugin)
{
eog_debug_message (DEBUG_PLUGINS, "EogExiv2RatingPlugin initializing");
plugin->exifproxy = new _ExifProxy();
}

static void
eog_exiv2_ratings_plugin_dispose (GObject *object)
{
EogExiv2RatingPlugin *plugin = EOG_EXIV_XMP_RATING_PLUGIN (object);

eog_debug_message (DEBUG_PLUGINS, "EogExiv2RatingPlugin disposing");

if (plugin->window != NULL)
{
g_object_unref (plugin->window);
plugin->window = NULL;

if (plugin->exifproxy) {
const _ExifProxy::History& h = plugin->exifproxy->history();
for ( _ExifProxy::History::const_iterator i=h.begin(); i!=h.end(); ++i)
{
cout << *i << endl;
}
}
delete plugin->exifproxy;
plugin->exifproxy = NULL;
}

G_OBJECT_CLASS (eog_exiv2_ratings_plugin_parent_class)->dispose (object);
}

static void
eog_exiv2_ratings_plugin_update_action_state (EogExiv2RatingPlugin *plugin, EogThumbView *view)
{
GAction *action;
EogThumbView *thumbview;
gboolean enable = FALSE;

thumbview = view == NULL ? EOG_THUMB_VIEW (eog_window_get_thumb_view (plugin->window)) : view;

if (G_LIKELY (thumbview))
{
enable = (eog_thumb_view_get_n_selected (thumbview) != 0);
//enable = (eog_thumb_view_get_first_selected_image(thumbview) != 0);
if (enable) {
//plugin->exifproxy->ref(*plugin->window); <-- this is the prev file before the change
plugin->exifproxy->ref(*thumbview);
//cout << *plugin->exifproxy << endl;
_upd_statusbar_exif(GTK_STATUSBAR(plugin->statusbar_exif),
NULL,
plugin->exifproxy->valid() ?
(plugin->exifproxy->rated() ? "rated" : "un-rated") : "-/-");
}
}

action = g_action_map_lookup_action (G_ACTION_MAP (plugin->window),
EOG_EXIV_XMP_RATING_PLUGIN_ACTION);
g_simple_action_set_enabled (G_SIMPLE_ACTION (action), enable);
}

static void
_selection_changed_cb (EogThumbView *thumbview, gpointer user_data)
{
EogExiv2RatingPlugin *plugin = EOG_EXIV_XMP_RATING_PLUGIN (user_data);

if (G_LIKELY (plugin)) {
eog_exiv2_ratings_plugin_update_action_state (plugin, thumbview);
}
}

static void
eog_exiv2_ratings_plugin_activate (EogWindowActivatable *activatable)
{
const gchar * const accel_keys[] = { "R", NULL };
EogExiv2RatingPlugin *plugin = EOG_EXIV_XMP_RATING_PLUGIN (activatable);
GMenu *model, *menu;
GMenuItem *item;
GSimpleAction *action;


GtkWidget *statusbar = eog_window_get_statusbar(plugin->window);
plugin->statusbar_exif = gtk_statusbar_new();
gtk_widget_set_size_request (plugin->statusbar_exif, 100, -1);
gtk_box_pack_end (GTK_BOX(statusbar), plugin->statusbar_exif, FALSE, FALSE, 0);


eog_debug (DEBUG_PLUGINS);

model= eog_window_get_gear_menu_section (plugin->window,
"plugins-section");

g_return_if_fail (G_IS_MENU (model));

/* Setup and inject action */
action = g_simple_action_new (EOG_EXIV_XMP_RATING_PLUGIN_ACTION, NULL);
#ifndef MULTI_CB
g_signal_connect(action, "activate", G_CALLBACK (_exiv2_rating_set_cb), plugin);
g_action_map_add_action (G_ACTION_MAP (plugin->window), G_ACTION (action));
#else
g_action_map_add_action_entries(G_ACTION_MAP(plugin->window),
actions, G_N_ELEMENTS(actions), plugin->window);
#endif

g_object_unref (action);

g_signal_connect (G_OBJECT (eog_window_get_thumb_view (plugin->window)),
"selection-changed",
G_CALLBACK (_selection_changed_cb),
plugin);
eog_exiv2_ratings_plugin_update_action_state (plugin, EOG_THUMB_VIEW (eog_window_get_thumb_view (plugin->window)));

#if 0
/* Append entry to the window's gear menu */
menu = g_menu_new ();
g_menu_append (menu, "Add Exif Rating",
"win." EOG_EXIV_XMP_RATING_PLUGIN_ACTION);

item = g_menu_item_new_section (NULL, G_MENU_MODEL (menu));
g_menu_item_set_attribute (item, "id",
"s", EOG_EXIV_XMP_RATING_PLUGIN_MENU_ID);
g_menu_item_set_attribute (item, G_MENU_ATTRIBUTE_ICON,
"s", "view-refresh-symbolic");
g_menu_append_item (model, item);
g_object_unref (item);

g_object_unref (menu);
#endif

/* Define accelerator keys */
gtk_application_set_accels_for_action (GTK_APPLICATION (EOG_APP),
"win." EOG_EXIV_XMP_RATING_PLUGIN_ACTION,
accel_keys);
}

static void
eog_exiv2_ratings_plugin_deactivate (EogWindowActivatable *activatable)
{
const gchar * const empty_accels[1] = { NULL };
EogExiv2RatingPlugin *plugin = EOG_EXIV_XMP_RATING_PLUGIN (activatable);
GMenu *menu;
GMenuModel *model;
gint i;

eog_debug (DEBUG_PLUGINS);

#if 0
menu = eog_window_get_gear_menu_section (plugin->window,
"plugins-section");

g_return_if_fail (G_IS_MENU (menu));

/* Remove menu entry */
model = G_MENU_MODEL (menu);
for (i = 0; i < g_menu_model_get_n_items (model); i++) {
gchar *id;
if (g_menu_model_get_item_attribute (model, i, "id", "s", &id)) {
const gboolean found =
(g_strcmp0 (id, EOG_EXIV_XMP_RATING_PLUGIN_MENU_ID) == 0);
g_free (id);

if (found) {
g_menu_remove (menu, i);
break;
}
}
}
#endif

/* Unset accelerator */
gtk_application_set_accels_for_action(GTK_APPLICATION (EOG_APP),
"win." EOG_EXIV_XMP_RATING_PLUGIN_ACTION,
empty_accels);

/* Disconnect selection-changed handler as the thumbview would
* otherwise still cause callbacks during its own disposal */
g_signal_handlers_disconnect_by_func (eog_window_get_thumb_view (plugin->window),
_selection_changed_cb, plugin);

/* Finally remove action */
g_action_map_remove_action (G_ACTION_MAP (plugin->window),
EOG_EXIV_XMP_RATING_PLUGIN_ACTION);

GtkWidget *statusbar = eog_window_get_statusbar (plugin->window);
gtk_container_remove(GTK_CONTAINER (statusbar), plugin->statusbar_exif);
}

static void
eog_exiv2_ratings_plugin_class_init (EogExiv2RatingPluginClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);

object_class->dispose= eog_exiv2_ratings_plugin_dispose;
object_class->set_property = eog_exiv2_ratings_plugin_set_property;
object_class->get_property = eog_exiv2_ratings_plugin_get_property;

g_object_class_override_property (object_class, PROP_WINDOW, "window");
}

static void
eog_exiv2_ratings_plugin_class_finalize (EogExiv2RatingPluginClass *klass)
{
}

static void
eog_window_activatable_iface_init (EogWindowActivatableInterface *iface)
{
iface->activate = eog_exiv2_ratings_plugin_activate;
iface->deactivate = eog_exiv2_ratings_plugin_deactivate;
}

extern "C"
{
G_MODULE_EXPORT void
peas_register_types (PeasObjectModule *module)
{
eog_exiv2_ratings_plugin_register_type (G_TYPE_MODULE (module));
peas_object_module_register_extension_type (module,
EOG_TYPE_WINDOW_ACTIVATABLE,
EOG_TYPE_EXIV_XMP_RATING_PLUGIN);
}
}

No comments:

Post a Comment