xmpedit

GTK+ editor for XMP metadata embedded in images
git clone https://code.djc.id.au/git/xmpedit/
commit 4d0dc0d97e46cfc40ed2653b7994b709cdf501f3
parent 6dda0306ea194e4b39f1b418ebd497a06b484046
Author: Dan Callaghan <djc@djc.id.au>
Date:   Sat, 11 Sep 2010 21:08:06 +1000

write XMP data to files (using exiv2 directly, instead of gexiv2)

Diffstat:
Mbuild | 104++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/ImageMetadata.vala | 36+++++++++++++++++++++++++++++-------
Asrc/evix2-glib.cpp | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exiv2-glib.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exiv2-tests.vala | 23+++++++++++++++++++++++
Msrc/xmpedit.vala | 1+
Atestdata/24-06-06_1449.jpg | 0
Avapi/exiv2.vapi | 18++++++++++++++++++
8 files changed, 310 insertions(+), 29 deletions(-)
diff --git a/build b/build
@@ -8,35 +8,95 @@ Because waf, SCons, autotools, and CMake all made me angry.
 import sys
 import os
 import subprocess
+import errno
 
 def invoke(command):
     print(' '.join(command))
     subprocess.check_call(command)
 
-def files_under(path):
+def files_under(*paths):
     result = []
-    for dirpath, dirnames, filenames in os.walk(path):
-        result.extend(os.path.join(dirpath, filename) for filename in filenames)
+    for path in paths:
+        for dirpath, dirnames, filenames in os.walk(path):
+            result.extend(os.path.join(dirpath, filename) for filename in filenames)
     return result
 
-def compile_vala(sources, target, pkgs=[], defs=[], vapidirs=[]):
-    invoke(['valac', '-g', '--save-temps', '-d', 'target', '-o', target] +
-           ['--Xcc=-g', '--Xcc=-Wall', '--Xcc=-O'] +
-           ['--Xcc=-Isrc', '--Xcc=-Ilib/genx'] + # XXX generalise
-           ['--pkg=%s' % pkg for pkg in pkgs] +
-           ['--define=%s' % define for define in defs] +
-           ['--vapidir=%s' % vapidir for vapidir in vapidirs] +
-           sources)
-
-def main():
-    pkgs = ['gtk+-2.0', 'gee-1.0', 'gexiv2', 'libxml-2.0', 'libsoup-2.4', 'genx']
-    vapidirs = ['vapi']
-    main_sources = [f for f in files_under('src') if f.endswith('.vala') or f.endswith('.c')]
-    lib_sources = [f for f in files_under('lib') if f.endswith('.c')]
-    compile_vala(sources=main_sources + lib_sources, target='xmpedit', pkgs=pkgs, vapidirs=vapidirs, defs=['DEBUG'])
-
-    compile_vala(sources=main_sources + lib_sources, target='xmpedit_test', pkgs=pkgs, vapidirs=vapidirs, defs=['DEBUG', 'TEST'])
-    invoke(['gtester', '--verbose', 'target/xmpedit_test'])
+def ensure_dir(path):
+    if not os.path.isdir(path):
+        os.mkdir(path)
+
+def replace_ext(path, old, new):
+    if not path.endswith(old):
+        raise ValueError('Path %r does not end with %r' % (path, old))
+    return path[:-len(old)] + new
+
+class Builder(object):
+
+    def __init__(self):
+        self.pkgs = ['gtk+-2.0', 'gee-1.0', 'libxml-2.0', 'libsoup-2.4', 'exiv2']
+        self.vala_sources = [f for f in files_under('src', 'lib') if f.endswith('.vala')]
+        self.vala_pkgs = self.pkgs + ['genx']
+        self.vala_vapidirs = ['vapi']
+        self.vala_flags = ['-g']
+        self.c_sources = [f for f in files_under('src', 'lib') if f.endswith('.c')]
+        self.c_flags = ['-g', '-Wall', '-Wno-pointer-sign', '-O']
+        self.c_includes = ['src', 'lib/genx']
+        self.cpp_sources = [f for f in files_under('src', 'lib') if f.endswith('.cpp')]
+        self.cpp_flags = ['-g', '-Wall', '-O']
+        self.cpp_includes = self.c_includes
+        self.objects = []
+        self.ld_flags = []
+        for pkg in self.pkgs:
+            cflags = subprocess.check_output(['pkg-config', '--cflags', pkg]).decode('ascii').split()
+            self.c_flags.extend(cflags)
+            self.cpp_flags.extend(cflags)
+            self.ld_flags.extend(subprocess.check_output(['pkg-config', '--libs', pkg]).decode('ascii').split())
+    
+    def compile(self):
+        self.compile_vala(defs=['DEBUG'])
+        self.compile_c()
+        self.compile_cpp()
+        self.link(target='xmpedit')
+    
+    def test(self):
+        self.compile_vala(defs=['DEBUG', 'TEST'])
+        self.compile_c()
+        self.compile_cpp()
+        self.link(target='xmpedit_test')
+        invoke(['gtester', '--verbose', 'target/xmpedit_test'])
+
+    def compile_vala(self, defs=[]):
+        invoke(['valac', '-C', '-d', 'target/valac'] +
+               self.vala_flags +
+               ['--pkg=%s' % pkg for pkg in self.vala_pkgs] +
+               ['--define=%s' % define for define in defs] +
+               ['--vapidir=%s' % vapidir for vapidir in self.vala_vapidirs] +
+               self.vala_sources)
+        self.c_sources.extend(os.path.join('target', 'valac', replace_ext(source, '.vala', '.c'))
+                for source in self.vala_sources)
+     
+    def compile_c(self):
+        ensure_dir(os.path.join('target', 'cc'))
+        for source in self.c_sources:
+            out = os.path.join('target', 'cc', replace_ext(os.path.basename(source), '.c', '.o'))
+            invoke(['gcc'] + self.c_flags +
+                   ['-I%s' % inc for inc in self.c_includes] +
+                   ['-c', source, '-o', out])
+            self.objects.append(out)
+     
+    def compile_cpp(self):
+        ensure_dir(os.path.join('target', 'cc'))
+        for source in self.cpp_sources:
+            out = os.path.join('target', 'cc', replace_ext(os.path.basename(source), '.cpp', '.o'))
+            invoke(['g++'] + self.cpp_flags +
+                   ['-I%s' % inc for inc in self.cpp_includes] +
+                   ['-c', source, '-o', out])
+            self.objects.append(out)
+    
+    def link(self, target):
+        invoke(['gcc'] + self.ld_flags + self.objects +
+               ['-o', os.path.join('target', target)])
 
 if __name__ == '__main__':
-    main()
+    Builder().compile()
+    Builder().test()
diff --git a/src/ImageMetadata.vala b/src/ImageMetadata.vala
@@ -160,6 +160,8 @@ public class ImageMetadata : Object, Gtk.TreeModel {
 
     public string path { get; construct; }
     public Gee.List<PropertyEditor> properties { get; construct; }
+    private Exiv2.Image image;
+    private size_t xmp_packet_size;
     private RDF.Graph graph;
     private RDF.URIRef subject;
     
@@ -180,10 +182,12 @@ public class ImageMetadata : Object, Gtk.TreeModel {
         }
     }
     
-    public void load() throws GLib.Error {
-        var exiv_metadata = new GExiv2.Metadata();
-        exiv_metadata.open_path(path);
-        string xmp = exiv_metadata.get_xmp_packet();
+    public void load() {
+        return_if_fail(image == null); // only call this once
+        image = new Exiv2.Image.from_path(path);
+        image.read_metadata();
+        unowned string xmp = image.xmp_packet;
+        xmp_packet_size = xmp.size();
 #if DEBUG
         stderr.puts("=== Extracted XMP packet:\n");
         stderr.puts(xmp);
@@ -210,14 +214,32 @@ public class ImageMetadata : Object, Gtk.TreeModel {
     }
     
     public void save() {
+        return_if_fail(image != null); // only call after successful loading
+        // XXX shouldn't write if not dirty!
 #if DEBUG
         stderr.puts("=== Final RDF graph:\n");
         foreach (var s in graph.get_statements())
             stderr.puts(@"$s\n");
-        stderr.puts("=== Serialized RDF XML:\n");
-        stderr.puts(graph.to_xml(subject));
 #endif
-        // XXX actually write it out
+        var xml = new StringBuilder.sized(xmp_packet_size);
+        xml.append("<?xpacket begin=\"\xef\xbb\xbf\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>" +
+                """<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="xmpedit 0.0-dev">""");
+        xml.append(graph.to_xml(subject));
+        var new_size = xml.str.size() + 12; // plus </x:xmpmeta>
+        size_t padding;
+        if (new_size <= xmp_packet_size)
+            padding = xmp_packet_size - new_size;
+        else
+            padding = new_size + 1024;
+        for (size_t i = 0; i < padding; i ++)
+            xml.append_c(' ');
+        xml.append("""</x:xmpmeta>""");
+#if DEBUG
+        stderr.puts("=== Serialized XMP packet:\n");
+        stderr.puts(xml.str);
+#endif
+        image.xmp_packet = xml.str;
+        image.write_metadata();
     }
     
     /****** TREEMODEL IMPLEMENTATION STUFF **********/
diff --git a/src/evix2-glib.cpp b/src/evix2-glib.cpp
@@ -0,0 +1,110 @@
+
+#include <exiv2/image.hpp>
+#include "exiv2-glib.h"
+
+G_DEFINE_TYPE (Exiv2Image, exiv2_image, G_TYPE_OBJECT)
+
+#define GET_PRIVATE(o) \
+  (G_TYPE_INSTANCE_GET_PRIVATE ((o), EXIV2_TYPE_IMAGE, Exiv2ImagePrivate))
+  
+typedef struct {
+    Exiv2::Image *image;
+} Exiv2ImagePrivate;
+
+static void exiv2_image_get_property(GObject *object, guint property_id,
+        GValue *value, GParamSpec *pspec) {
+    switch (property_id) {
+        default:
+            G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+static void exiv2_image_set_property(GObject *object, guint property_id,
+        const GValue *value, GParamSpec *pspec) {
+    switch (property_id) {
+        default:
+            G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+static void exiv2_image_dispose(GObject *object) {
+    G_OBJECT_CLASS(exiv2_image_parent_class)->dispose(object);
+}
+
+static void exiv2_image_finalize(GObject *object) {
+    G_OBJECT_CLASS(exiv2_image_parent_class)->finalize(object);
+}
+
+static void exiv2_image_class_init(Exiv2ImageClass *klass) {
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    g_type_class_add_private(klass, sizeof(Exiv2ImagePrivate));
+
+    object_class->get_property = exiv2_image_get_property;
+    object_class->set_property = exiv2_image_set_property;
+    object_class->dispose = exiv2_image_dispose;
+    object_class->finalize = exiv2_image_finalize;
+}
+
+static void exiv2_image_init(Exiv2Image *self) {
+}
+
+Exiv2Image *exiv2_image_new_from_path(gchar *path) {
+    g_return_val_if_fail(path != NULL, NULL);
+    Exiv2Image *self = (Exiv2Image *)g_object_new(EXIV2_TYPE_IMAGE, NULL);
+    g_return_val_if_fail(self != NULL, NULL);
+    Exiv2ImagePrivate *priv = GET_PRIVATE(self);
+    try {
+        priv->image = Exiv2::ImageFactory::open(path).release();
+    } catch (Exiv2::Error e) {
+        g_critical("unhandled"); // XXX
+    }
+    return self;
+}
+
+void exiv2_image_read_metadata(Exiv2Image *self) {
+    g_return_if_fail(self != NULL);
+    Exiv2ImagePrivate *priv = GET_PRIVATE(self);
+    g_return_if_fail(priv->image != NULL);
+    try {
+        priv->image->readMetadata();
+    } catch (Exiv2::Error e) {
+        g_critical("unhandled"); // XXX
+    }
+}
+
+void exiv2_image_write_metadata(Exiv2Image *self) {
+    g_return_if_fail(self != NULL);
+    Exiv2ImagePrivate *priv = GET_PRIVATE(self);
+    g_return_if_fail(priv->image != NULL);
+    try {
+        priv->image->writeMetadata();
+    } catch (Exiv2::Error e) {
+        g_critical("unhandled"); // XXX
+    }
+}
+
+const gchar *exiv2_image_get_xmp_packet(Exiv2Image *self) {
+    g_return_val_if_fail(self != NULL, NULL);
+    Exiv2ImagePrivate *priv = GET_PRIVATE(self);
+    g_return_val_if_fail(priv->image != NULL, NULL);
+    try {
+        const std::string& xmp_packet = priv->image->xmpPacket();
+        if (!xmp_packet.empty())
+            return xmp_packet.c_str();
+    } catch (Exiv2::Error e) {
+        g_critical("unhandled"); // XXX
+    }
+    return NULL;
+}
+
+void exiv2_image_set_xmp_packet(Exiv2Image *self, const gchar *xmp_packet) {
+    g_return_if_fail(self != NULL);
+    Exiv2ImagePrivate *priv = GET_PRIVATE(self);
+    g_return_if_fail(priv->image != NULL);
+    try {
+        priv->image->setXmpPacket(xmp_packet);
+    } catch (Exiv2::Error e) {
+        g_critical("unhandled"); // XXX
+    }
+}
diff --git a/src/exiv2-glib.h b/src/exiv2-glib.h
@@ -0,0 +1,47 @@
+
+#ifndef _EXIV2_GLIB
+#define _EXIV2_GLIB
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define EXIV2_TYPE_IMAGE exiv2_image_get_type()
+
+#define EXIV2_IMAGE(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj), EXIV2_TYPE_IMAGE, Exiv2Image))
+
+#define EXIV2_IMAGE_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST ((klass), EXIV2_TYPE_IMAGE, Exiv2ImageClass))
+
+#define EXIV2_IS_IMAGE(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EXIV2_TYPE_IMAGE))
+
+#define EXIV2_IS_IMAGE_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE ((klass), EXIV2_TYPE_IMAGE))
+
+#define EXIV2_IMAGE_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), EXIV2_TYPE_IMAGE, Exiv2ImageClass))
+
+typedef struct {
+    GObject parent;
+} Exiv2Image;
+
+typedef struct {
+    GObjectClass parent_class;
+} Exiv2ImageClass;
+
+GType exiv2_image_get_type(void);
+
+Exiv2Image *exiv2_image_new_from_path(gchar *path);
+
+void exiv2_image_read_metadata(Exiv2Image *self);
+void exiv2_image_write_metadata(Exiv2Image *self);
+
+const gchar *exiv2_image_get_xmp_packet(Exiv2Image *self);
+void exiv2_image_set_xmp_packet(Exiv2Image *self, const gchar *xmp_packet);
+
+G_END_DECLS
+
+#endif /* inclusion guard */
+
diff --git a/src/exiv2-tests.vala b/src/exiv2-tests.vala
@@ -0,0 +1,23 @@
+
+#if TEST
+
+namespace Exiv2 {
+
+namespace Tests {
+
+public void test_load_xmp() {
+    var image = new Image.from_path("testdata/24-06-06_1449.jpg");
+    image.read_metadata();
+    assert(Checksum.compute_for_string(ChecksumType.SHA1, image.xmp_packet)
+            == "fb357e9a9e9fb5f4481234d2f8f5e59275fc07af");
+}
+
+public void register_tests() {
+    Test.add_func("/exiv2/test_load_xmp", test_load_xmp);
+}
+
+}
+
+}
+
+#endif
diff --git a/src/xmpedit.vala b/src/xmpedit.vala
@@ -6,6 +6,7 @@ namespace Xmpedit {
 public int main(string[] args) {
     Test.init(ref args);
     RDF.register_tests();
+    Exiv2.Tests.register_tests();
     Test.run();
     return 0;
 }
diff --git a/testdata/24-06-06_1449.jpg b/testdata/24-06-06_1449.jpg
Binary files differ.
diff --git a/vapi/exiv2.vapi b/vapi/exiv2.vapi
@@ -0,0 +1,18 @@
+// TODO use vala-gen-introspect
+
+[CCode (cheader_filename = "exiv2-glib.h")]
+namespace Exiv2 {
+
+    [CCode (ref_function = "g_object_ref", unref_function = "g_object_unref")]
+    public class Image {
+    
+        public Image.from_path(string path);
+        
+        public string? xmp_packet { get; set; }
+        
+        public void read_metadata();
+        public void write_metadata();
+    
+    }
+    
+}