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:
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();
+
+ }
+
+}