LeoHsiao1 / pyexiv2

Read and write image metadata, including EXIF, IPTC, XMP, ICC Profile.
GNU General Public License v3.0
206 stars 39 forks source link

modify_xmp() errors with 'Composite nodes can't have values' #126

Closed molexx closed 1 year ago

molexx commented 1 year ago

Sometimes writing XMP metadata to an image using modify_xmp() raises this error:

RuntimeError: XMP Toolkit error 102: Composite nodes can't have values

read_raw_xmp() and then modify_raw_xmp() works fine but I would like to edit some of the metadata in between read/modify and the dict is much more usable than trying to parse the XML and back again (during which I would probably cause more problems).

This can be recreated using this snippet which uses the IPTC's example jpg with lots of metadata:

import urllib3
import pyexiv2

url = "https://iptc.org/std/photometadata/examples/IPTC-PhotometadataRef-Std2023.1.jpg"

contents = urllib3.PoolManager().request('GET', url)

with pyexiv2.ImageData(contents.data) as pyexiv2_imagedata:
    # read xmp data as a dict
    xmp_dict = pyexiv2_imagedata.read_xmp()
    #xmp_raw = pyexiv2_imagedata.read_raw_xmp()

    # ...perhaps do some modifications to the dict...

    # write the xmp dict back to the ImageData
    pyexiv2_imagedata.modify_xmp(xmp_dict)
    #pyexiv2_imagedata.modify_raw_xmp(xmp_raw)
LeoHsiao1 commented 1 year ago

Hi, thanks for reporting the bug.

Here are the steps of my analysis. First, I executed the following code to locate the XMP key that could not be modified:

import pyexiv2
img = pyexiv2.Image('test.jpg')
xmp = img.read_xmp()
for k,v in xmp.items():
    print(f'"{k}": {v}')
    img.modify_xmp({k: v})

Then I found this particular key:

>>> img.clear_xmp()
>>> img.modify_xmp({"Xmp.iptcExt.ArtworkOrObject": 'type="Bag"'})
>>> img.modify_xmp({"Xmp.iptcExt.ArtworkOrObject[1]": 'type="Struct"'})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    self.img.modify_xmp(self._dumps(data), encoding)
RuntimeError: XMP Toolkit error 102: Composite nodes can't have values
Failed to encode XMP metadata.

I checked the docs of exiv2. This key, which contains [1], is a struct that holds the sub-elements of an array. https://exiv2.org/manpage.html#set_xmp_struct

In order to modify the key in this format, it is necessary to change

img.modify_xmp({"Xmp.iptcExt.ArtworkOrObject": 'type="Bag"'})

to

img.modify_xmp({"Xmp.iptcExt.ArtworkOrObject": ['']})

I'll release a new version of pyexiv2 that automates this conversion.

LeoHsiao1 commented 1 year ago

In short, this key is a special data structure used by exiv2 to read and write XMP. You can also get the raw XMP data via img.read_raw_xmp(), then extract its sub-elements via import lxml or import re.

molexx commented 1 year ago

Thank you!

I have been trying to parse the XML to edit it - I need to add elements some way down the structure and get the namespaces right which is tripping me up so I'd much prefer to not reinvent the wheel :-)

LeoHsiao1 commented 1 year ago

I just released pyexiv2 v2.9.0 Now when calling img.read_xmp(), {"Xmp.iptcExt.ArtworkOrObject": 'type="Bag"'} is automatically converted to {"Xmp.iptcExt.ArtworkOrObject": ['']}. The this value can be entered into img.modify_xmp(??). If img.modify_xmp({"Xmp.iptcExt.ArtworkOrObject": 'type="Bag"'}) is executed, it will still fail.

molexx commented 1 year ago

Ah works great, thank you!