aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOlivier Tilloy <olivier@tilloy.net>2010-12-17 21:01:59 +0100
committerOlivier Tilloy <olivier@tilloy.net>2010-12-17 21:01:59 +0100
commit185f414aa8ceb4642b73d065dee3eb70570bb459 (patch)
tree9abfb80c1c0364f17ae0037febb339a8281f8d77
parenta9fd5a30104ae3a2ceec72bfcf8b6c4ed058ef17 (diff)
parentcc22de6ce7bec06bfa6843fdc8a010d9fe6f0061 (diff)
downloadpyexiv2-185f414aa8ceb4642b73d065dee3eb70570bb459.tar.gz
API to (un)register custom XMP namespaces.
A known limitation of this implementation is that only simple text values can be written to tags in a custom namespace.
-rw-r--r--doc/api.rst3
-rw-r--r--doc/tutorial.rst13
-rw-r--r--src/exiv2wrapper.cpp59
-rw-r--r--src/exiv2wrapper.hpp6
-rw-r--r--src/exiv2wrapper_python.cpp4
-rw-r--r--src/pyexiv2/__init__.py3
-rw-r--r--src/pyexiv2/xmp.py66
-rwxr-xr-xtest/TestsRunner.py3
-rw-r--r--test/xmp.py102
9 files changed, 256 insertions, 3 deletions
diff --git a/doc/api.rst b/doc/api.rst
index a06b552..d58eb60 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -43,6 +43,9 @@ pyexiv2.xmp
###########
.. module:: pyexiv2.xmp
+.. autofunction:: register_namespace
+.. autofunction:: unregister_namespace
+.. autofunction:: unregister_namespaces
.. autoexception:: XmpValueError
.. autoclass:: XmpTag
:members: key, type, name, title, description, raw_value, value
diff --git a/doc/tutorial.rst b/doc/tutorial.rst
index 9f5a418..709b686 100644
--- a/doc/tutorial.rst
+++ b/doc/tutorial.rst
@@ -240,6 +240,19 @@ If the tag was not present, one is created and its value is set::
>>> metadata[key] = value
+If you need to write custom metadata, you can register a custom XMP namespace::
+
+ >>> pyexiv2.xmp.register_namespace('http://example.org/foo/', 'foo')
+ >>> metadata['Xmp.foo.bar'] = 'baz'
+
+Note that a limitation of the current implementation is that only simple text
+values can be written to tags in a custom namespace.
+
+A custom namespace can be unregistered. This has the effect of invalidating all
+tags in this namespace for images that have not been written back yet::
+
+ >>> pyexiv2.xmp.unregister_namespace('http://example.org/foo/')
+
Accessing embedded previews
###########################
diff --git a/src/exiv2wrapper.cpp b/src/exiv2wrapper.cpp
index 293bb0b..4df23e9 100644
--- a/src/exiv2wrapper.cpp
+++ b/src/exiv2wrapper.cpp
@@ -34,6 +34,9 @@
#define NON_REPEATABLE 102
#define KEY_NOT_FOUND 103
#define INVALID_VALUE 104
+#define EXISTING_PREFIX 105
+#define BUILTIN_NS 106
+#define NOT_REGISTERED 107
// Custom macros
#define CHECK_METADATA_READ \
@@ -1310,6 +1313,15 @@ void translateExiv2Error(Exiv2::Error const& error)
case INVALID_VALUE:
PyErr_SetString(PyExc_ValueError, "Invalid value");
break;
+ case EXISTING_PREFIX:
+ PyErr_SetString(PyExc_KeyError, "A namespace with this prefix already exists");
+ break;
+ case BUILTIN_NS:
+ PyErr_SetString(PyExc_KeyError, "Cannot unregister a builtin namespace");
+ break;
+ case NOT_REGISTERED:
+ PyErr_SetString(PyExc_KeyError, "No namespace registered under this name");
+ break;
// Default handler
default:
@@ -1317,5 +1329,52 @@ void translateExiv2Error(Exiv2::Error const& error)
}
}
+
+void registerXmpNs(const std::string& name, const std::string& prefix)
+{
+ try
+ {
+ const std::string& ns = Exiv2::XmpProperties::ns(prefix);
+ }
+ catch (Exiv2::Error& error)
+ {
+ // No namespace exists with the requested prefix, it is safe to
+ // register a new one.
+ Exiv2::XmpProperties::registerNs(name, prefix);
+ return;
+ }
+ throw Exiv2::Error(EXISTING_PREFIX, prefix);
+}
+
+void unregisterXmpNs(const std::string& name)
+{
+ const std::string& prefix = Exiv2::XmpProperties::prefix(name);
+ if (prefix != "")
+ {
+ Exiv2::XmpProperties::unregisterNs(name);
+ try
+ {
+ const Exiv2::XmpNsInfo* info = Exiv2::XmpProperties::nsInfo(prefix);
+ }
+ catch (Exiv2::Error& error)
+ {
+ // The namespace has been successfully unregistered.
+ return;
+ }
+ // The namespace hasn’t been unregistered because it’s builtin.
+ throw Exiv2::Error(BUILTIN_NS, name);
+ }
+ else
+ {
+ throw Exiv2::Error(NOT_REGISTERED, name);
+ }
+}
+
+void unregisterAllXmpNs()
+{
+ // Unregister all custom namespaces.
+ Exiv2::XmpProperties::unregisterNs();
+}
+
} // End of namespace exiv2wrapper
diff --git a/src/exiv2wrapper.hpp b/src/exiv2wrapper.hpp
index 93b7823..f3a0bd6 100644
--- a/src/exiv2wrapper.hpp
+++ b/src/exiv2wrapper.hpp
@@ -278,6 +278,12 @@ private:
// Translate an Exiv2 generic exception into a Python exception
void translateExiv2Error(Exiv2::Error const& error);
+
+// Functions to manipulate custom XMP namespaces
+void registerXmpNs(const std::string& name, const std::string& prefix);
+void unregisterXmpNs(const std::string& name);
+void unregisterAllXmpNs();
+
} // End of namespace exiv2wrapper
#endif
diff --git a/src/exiv2wrapper_python.cpp b/src/exiv2wrapper_python.cpp
index a718afe..ac51fae 100644
--- a/src/exiv2wrapper_python.cpp
+++ b/src/exiv2wrapper_python.cpp
@@ -154,5 +154,9 @@ BOOST_PYTHON_MODULE(libexiv2python)
.def("_setExifThumbnailFromFile", &Image::setExifThumbnailFromFile)
.def("_setExifThumbnailFromData", &Image::setExifThumbnailFromData)
;
+
+ def("_registerXmpNs", registerXmpNs, args("name", "prefix"));
+ def("_unregisterXmpNs", unregisterXmpNs, args("name"));
+ def("_unregisterAllXmpNs", unregisterAllXmpNs);
}
diff --git a/src/pyexiv2/__init__.py b/src/pyexiv2/__init__.py
index 54d7ebe..9aa3185 100644
--- a/src/pyexiv2/__init__.py
+++ b/src/pyexiv2/__init__.py
@@ -62,7 +62,8 @@ import libexiv2python
from pyexiv2.metadata import ImageMetadata
from pyexiv2.exif import ExifValueError, ExifTag, ExifThumbnail
from pyexiv2.iptc import IptcValueError, IptcTag
-from pyexiv2.xmp import XmpValueError, XmpTag
+from pyexiv2.xmp import XmpValueError, XmpTag, register_namespace, \
+ unregister_namespace, unregister_namespaces
from pyexiv2.preview import Preview
from pyexiv2.utils import FixedOffset, Rational, NotifyingList, \
undefined_to_string, string_to_undefined, \
diff --git a/src/pyexiv2/xmp.py b/src/pyexiv2/xmp.py
index f13db7b..8223a24 100644
--- a/src/pyexiv2/xmp.py
+++ b/src/pyexiv2/xmp.py
@@ -441,6 +441,18 @@ class XmpTag(object):
else:
raise XmpValueError(value, type)
+ elif type == '':
+ # Unknown type, assume string
+ if isinstance(value, unicode):
+ try:
+ return value.encode('utf-8')
+ except UnicodeEncodeError:
+ raise XmpValueError(value, type)
+ elif isinstance(value, str):
+ return value
+ else:
+ raise XmpValueError(value, type)
+
raise NotImplementedError('XMP conversion for type [%s]' % type)
def __str__(self):
@@ -455,3 +467,57 @@ class XmpTag(object):
right = self._raw_value
return '<%s = %s>' % (left, right)
+
+def register_namespace(name, prefix):
+ """
+ Register a custom XMP namespace.
+
+ Overriding the prefix of a known or previously registered namespace is not
+ allowed.
+
+ :param name: the name of the custom namespace (ending with a ``/``),
+ typically a URL (e.g. http://purl.org/dc/elements/1.1/)
+ :type name: string
+ :param prefix: the prefix for the custom namespace (keys in this namespace
+ will be in the form ``Xmp.{prefix}.{something}``)
+ :type prefix: string
+
+ :raise ValueError: if the name doesn’t end with a ``/``
+ :raise KeyError: if a namespace already exist with this prefix
+ """
+ if not name.endswith('/'):
+ raise ValueError('Name should end with a /')
+ libexiv2python._registerXmpNs(name, prefix)
+
+
+def unregister_namespace(name):
+ """
+ Unregister a custom XMP namespace.
+
+ A custom namespace is identified by its name, **not** by its prefix.
+
+ Attempting to unregister an unknown namespace raises an error, as does
+ attempting to unregister a builtin namespace.
+
+ :param name: the name of the custom namespace (ending with a ``/``),
+ typically a URL (e.g. http://purl.org/dc/elements/1.1/)
+ :type name: string
+
+ :raise ValueError: if the name doesn’t end with a ``/``
+ :raise KeyError: if the namespace is unknown or a builtin namespace
+ """
+ if not name.endswith('/'):
+ raise ValueError('Name should end with a /')
+ libexiv2python._unregisterXmpNs(name)
+
+
+def unregister_namespaces():
+ """
+ Unregister all custom XMP namespaces.
+
+ Builtin namespaces are not unregistered.
+
+ This function always succeeds.
+ """
+ libexiv2python._unregisterAllXmpNs()
+
diff --git a/test/TestsRunner.py b/test/TestsRunner.py
index 8c9f41a..8585295 100755
--- a/test/TestsRunner.py
+++ b/test/TestsRunner.py
@@ -34,7 +34,7 @@ from gps_coordinate import TestGPSCoordinate
from notifying_list import TestNotifyingList
from exif import TestExifTag
from iptc import TestIptcTag
-from xmp import TestXmpTag
+from xmp import TestXmpTag, TestXmpNamespaces
from metadata import TestImageMetadata
from buffer import TestBuffer
from encoding import TestEncodings
@@ -52,6 +52,7 @@ def run_unit_tests():
suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestExifTag))
suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestIptcTag))
suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestXmpTag))
+ suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestXmpNamespaces))
suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestImageMetadata))
suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestBuffer))
suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestEncodings))
diff --git a/test/xmp.py b/test/xmp.py
index 845b048..3379851 100644
--- a/test/xmp.py
+++ b/test/xmp.py
@@ -26,10 +26,13 @@
import unittest
-from pyexiv2.xmp import XmpTag, XmpValueError
+from pyexiv2.xmp import XmpTag, XmpValueError, register_namespace, \
+ unregister_namespace, unregister_namespaces
from pyexiv2.utils import FixedOffset, Rational, Fraction
+from pyexiv2.metadata import ImageMetadata
import datetime
+from testutils import EMPTY_JPG_DATA
class TestXmpTag(unittest.TestCase):
@@ -323,3 +326,100 @@ class TestXmpTag(unittest.TestCase):
self.failUnlessEqual(tag.type, 'Lang Alt')
self.failUnlessRaises(ValueError, tag._set_value, {})
+
+class TestXmpNamespaces(unittest.TestCase):
+
+ def setUp(self):
+ self.metadata = ImageMetadata.from_buffer(EMPTY_JPG_DATA)
+ self.metadata.read()
+
+ def test_not_registered(self):
+ self.assertEqual(len(self.metadata.xmp_keys), 0)
+ key = 'Xmp.foo.bar'
+ value = 'foobar'
+ self.assertRaises(KeyError, self.metadata.__setitem__, key, value)
+
+ def test_name_must_end_with_slash(self):
+ self.assertRaises(ValueError, register_namespace, 'foobar', 'foo')
+ self.assertRaises(ValueError, unregister_namespace, 'foobar')
+
+ def test_cannot_register_builtin(self):
+ self.assertRaises(KeyError, register_namespace, 'foobar/', 'dc')
+
+ def test_cannot_register_twice(self):
+ name = 'foobar/'
+ prefix = 'boo'
+ register_namespace(name, prefix)
+ self.assertRaises(KeyError, register_namespace, name, prefix)
+
+ def test_register_and_set(self):
+ register_namespace('foobar/', 'bar')
+ key = 'Xmp.bar.foo'
+ value = 'foobar'
+ self.metadata[key] = value
+ self.assert_(key in self.metadata.xmp_keys)
+
+ def test_can_only_set_text_values(self):
+ # At the moment custom namespaces only support setting simple text
+ # values.
+ register_namespace('foobar/', 'far')
+ key = 'Xmp.far.foo'
+ value = datetime.date.today()
+ self.assertRaises(XmpValueError, self.metadata.__setitem__, key, value)
+ value = datetime.datetime.now()
+ self.assertRaises(XmpValueError, self.metadata.__setitem__, key, value)
+ value = ['foo', 'bar']
+ self.assertRaises(XmpValueError, self.metadata.__setitem__, key, value)
+ value = {'x-default': 'foo', 'fr-FR': 'bar'}
+ self.assertRaises(XmpValueError, self.metadata.__setitem__, key, value)
+ value = 'simple text value'
+ self.metadata[key] = value
+
+ def test_cannot_unregister_builtin(self):
+ name = 'http://purl.org/dc/elements/1.1/' # DC builtin namespace
+ self.assertRaises(KeyError, unregister_namespace, name)
+
+ def test_cannot_unregister_inexistent(self):
+ name = 'boofar/'
+ self.assertRaises(KeyError, unregister_namespace, name)
+
+ def test_cannot_unregister_twice(self):
+ name = 'bleh/'
+ prefix = 'ble'
+ register_namespace(name, prefix)
+ unregister_namespace(name)
+ self.assertRaises(KeyError, unregister_namespace, name)
+
+ def test_unregister(self):
+ name = 'blah/'
+ prefix = 'bla'
+ register_namespace(name, prefix)
+ unregister_namespace(name)
+
+ def test_unregister_invalidates_keys_in_ns(self):
+ name = 'blih/'
+ prefix = 'bli'
+ register_namespace(name, prefix)
+ key = 'Xmp.%s.blu' % prefix
+ self.metadata[key] = 'foobar'
+ self.assert_(key in self.metadata.xmp_keys)
+ unregister_namespace(name)
+ self.assertRaises(KeyError, self.metadata.write)
+
+ def test_unregister_all_ns(self):
+ # Unregistering all custom namespaces will always succeed, even if there
+ # are no custom namespaces registered.
+ unregister_namespaces()
+
+ name = 'blop/'
+ prefix = 'blo'
+ register_namespace(name, prefix)
+ self.metadata['Xmp.%s.bar' % prefix] = 'foobar'
+ name2 = 'blup/'
+ prefix2 = 'blu'
+ register_namespace(name2, prefix2)
+ self.metadata['Xmp.%s.bar' % prefix2] = 'foobar'
+ unregister_namespaces()
+ self.assertRaises(KeyError, self.metadata.__setitem__, 'Xmp.%s.baz' % prefix, 'foobaz')
+ self.assertRaises(KeyError, self.metadata.__setitem__, 'Xmp.%s.baz' % prefix2, 'foobaz')
+