diff options
author | Olivier Tilloy <olivier@tilloy.net> | 2010-12-17 21:01:59 +0100 |
---|---|---|
committer | Olivier Tilloy <olivier@tilloy.net> | 2010-12-17 21:01:59 +0100 |
commit | 185f414aa8ceb4642b73d065dee3eb70570bb459 (patch) | |
tree | 9abfb80c1c0364f17ae0037febb339a8281f8d77 | |
parent | a9fd5a30104ae3a2ceec72bfcf8b6c4ed058ef17 (diff) | |
parent | cc22de6ce7bec06bfa6843fdc8a010d9fe6f0061 (diff) | |
download | pyexiv2-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.rst | 3 | ||||
-rw-r--r-- | doc/tutorial.rst | 13 | ||||
-rw-r--r-- | src/exiv2wrapper.cpp | 59 | ||||
-rw-r--r-- | src/exiv2wrapper.hpp | 6 | ||||
-rw-r--r-- | src/exiv2wrapper_python.cpp | 4 | ||||
-rw-r--r-- | src/pyexiv2/__init__.py | 3 | ||||
-rw-r--r-- | src/pyexiv2/xmp.py | 66 | ||||
-rwxr-xr-x | test/TestsRunner.py | 3 | ||||
-rw-r--r-- | test/xmp.py | 102 |
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') + |