aboutsummaryrefslogblamecommitdiffstats
path: root/test/metadata.py
blob: 177ec9da5257b2fd5c35122475c5d45848d66154 (plain) (tree)
1
2
3
4
5



                                                                                
                                                             




















                                                                                



                                          
                                                    

               

               
           
               
                                    
 



                                           
                                    

                                                           









                                                                            
                                  




                                                    




                          































                                                                                                        
                                                                                  
 
                        
                                                                            

                                                    
 



                                                     

                                             

                                    





                                                
                                      
                                      




                                                  

                                    





                                               
                                      
                                      




                                                                              




                                                
                                      
                                      


                                            





                               

                                                           
                                      



                                                           



                                                         
                                              






                                                                         







                                                                          

                                                         
                                                      
                                                            
                                                 
                                                        
                                                                     

                                                                                  



                                          

                                                         
                                                                                       
                                                 
                                                                     

                                                                                  



                                                         

                                                         
                               

                                                               
                                               
                                                 
                                                                     


                                                                              
 

                                                        





                                                                             
                                                             

                                              
                                                                 

                                                                              
 

                                              




                                                                            

                                                         
                                                    

                                                         
                                                   


                                          
                                   
                                                    



                                                               
                                                   






                               






                                                           



                                                         
                                              






                                                                         







                                                                          

                                                         
                                                             
                                                            
                                                 
                                                        
                                                                     

                                                                                   
                                       


                                          

                                                         
                                                                  
                                                 
                                                                     

                                                                                   
                                       


                                                         




                                                               
                                              
                                                 
                                                                     

                                                                               
                                           
 

                                                        





                                                                             
                                                             
                                              
                                           
                                                                 
                                                                               
                                       
 

                                              




                                                                            

                                                         
                                                    

                                                         
                                                   


                                          
                                         
                                                    



                                                               
                                                   






                              

                                                          
                                      



                                                          



                                                        
                                             






                                                                        







                                                                         

                                                        

                                                                          
                                                           
                                                
                                                       
                                                                    

                                                                                     



                                         

                                                        
                                                       
                                                
                                                                    

                                                                                  



                                                        

                                                        
                              

                                                              
                                                 
                                                
                                                                    

                                                                               

                                           

                                                       






                                                                             
                                                            

                                             
                                                                
                                                                                                
 

                                             




                                                                           

                                                        
                                                   

                                                        
                                                  


                                         
                              
                                                   



                                                              
                                                  






                               
                           
                                   
                                
                                              

                                         
                                              
                             
                                
                                             







                                                                            

                                            
                                               



                                                                   
                                         



                                                                   
                                                           




                                                                  
                                  



                                                                   
                                                    



                                                                   
                                                                        





                                                                  
                              
                               
                              
                                                       


                                                       
                                                       
                                                       
                              
                              
                                                      





                                                                            
 










                                                               




















                                                                   




                            
                                                              


                                 

                            




                                                                             

                                      


                                                                







                                                                                        
                                                                                 
 

                                                                                 
 

                                                                       













































                                                                                                 





















































                                                                   
                                 






































                                                                              












                                                            































































                                                                                 
# -*- coding: utf-8 -*-

# ******************************************************************************
#
# Copyright (C) 2009-2010 Olivier Tilloy <olivier@tilloy.net>
#
# This file is part of the pyexiv2 distribution.
#
# pyexiv2 is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# pyexiv2 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyexiv2; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA.
#
# Author: Olivier Tilloy <olivier@tilloy.net>
#
# ******************************************************************************

from pyexiv2.metadata import ImageMetadata
from pyexiv2.exif import ExifTag
from pyexiv2.iptc import IptcTag
from pyexiv2.xmp import XmpTag
from pyexiv2.utils import FixedOffset, make_fraction

import datetime
import os
import tempfile
import time
import unittest
from testutils import EMPTY_JPG_DATA


class TestImageMetadata(unittest.TestCase):

    def setUp(self):
        # Create an empty image file
        fd, self.pathname = tempfile.mkstemp(suffix='.jpg')
        os.write(fd, EMPTY_JPG_DATA)
        os.close(fd)
        # Write some metadata
        m = ImageMetadata(self.pathname)
        m.read()
        m['Exif.Image.Make'] = 'EASTMAN KODAK COMPANY'
        m['Exif.Image.DateTime'] = datetime.datetime(2009, 2, 9, 13, 33, 20)
        m['Iptc.Application2.Caption'] = ['blabla']
        m['Iptc.Application2.DateCreated'] = [datetime.date(2004, 7, 13)]
        m['Xmp.dc.format'] = ('image', 'jpeg')
        m['Xmp.dc.subject'] = ['image', 'test', 'pyexiv2']
        m.comment = 'Hello World!'
        m.write()
        self.metadata = ImageMetadata(self.pathname)

    def tearDown(self):
        os.remove(self.pathname)

    ######################
    # Test general methods
    ######################

    def test_not_read_raises(self):
        # http://bugs.launchpad.net/pyexiv2/+bug/687373
        self.assertRaises(IOError, self.metadata.write)
        self.assertRaises(IOError, self.metadata.__getattribute__, 'dimensions')
        self.assertRaises(IOError, self.metadata.__getattribute__, 'mime_type')
        self.assertRaises(IOError, self.metadata.__getattribute__, 'exif_keys')
        self.assertRaises(IOError, self.metadata.__getattribute__, 'iptc_keys')
        self.assertRaises(IOError, self.metadata.__getattribute__, 'xmp_keys')
        self.assertRaises(IOError, self.metadata._get_exif_tag, 'Exif.Image.Make')
        self.assertRaises(IOError, self.metadata._get_iptc_tag, 'Iptc.Application2.Caption')
        self.assertRaises(IOError, self.metadata._get_xmp_tag, 'Xmp.dc.format')
        self.assertRaises(IOError, self.metadata._set_exif_tag, 'Exif.Image.Make', 'foobar')
        self.assertRaises(IOError, self.metadata._set_iptc_tag, 'Iptc.Application2.Caption', ['foobar'])
        self.assertRaises(IOError, self.metadata._set_xmp_tag, 'Xmp.dc.format', ('foo', 'bar'))
        self.assertRaises(IOError, self.metadata._delete_exif_tag, 'Exif.Image.Make')
        self.assertRaises(IOError, self.metadata._delete_iptc_tag, 'Iptc.Application2.Caption')
        self.assertRaises(IOError, self.metadata._delete_xmp_tag, 'Xmp.dc.format')
        self.assertRaises(IOError, self.metadata._get_comment)
        self.assertRaises(IOError, self.metadata._set_comment, 'foobar')
        self.assertRaises(IOError, self.metadata._del_comment)
        self.assertRaises(IOError, self.metadata.__getattribute__, 'previews')
        other = ImageMetadata(self.pathname)
        self.assertRaises(IOError, self.metadata.copy, other)
        self.assertRaises(IOError, self.metadata.__getattribute__, 'buffer')
        thumb = self.metadata.exif_thumbnail
        self.assertRaises(IOError, thumb.__getattribute__, 'mime_type')
        self.assertRaises(IOError, thumb.__getattribute__, 'extension')
        self.assertRaises(IOError, thumb.write_to_file, '/tmp/foobar.jpg')
        self.assertRaises(IOError, thumb.erase)
        self.assertRaises(IOError, thumb.set_from_file, '/tmp/foobar.jpg')
        self.assertRaises(IOError, thumb.__getattribute__, 'data')
        self.assertRaises(IOError, thumb.__setattr__, 'data', EMPTY_JPG_DATA)
        self.assertRaises(IOError, self.metadata.__getattribute__, 'iptc_charset')

    def test_read(self):
        self.assertRaises(IOError, self.metadata.__getattribute__, '_image')
        self.metadata.read()
        self.failIfEqual(self.metadata._image, None)

    def test_read_nonexistent_file(self):
        metadata = ImageMetadata('idontexist')
        self.failUnlessRaises(IOError, metadata.read)

    def test_write_preserve_timestamps(self):
        stat = os.stat(self.pathname)
        atime = round(stat.st_atime)
        mtime = round(stat.st_mtime)
        metadata = ImageMetadata(self.pathname)
        metadata.read()
        metadata.comment = 'Yellow Submarine'
        time.sleep(1.1)
        metadata.write(preserve_timestamps=True)
        stat2 = os.stat(self.pathname)
        atime2 = round(stat2.st_atime)
        mtime2 = round(stat2.st_mtime)
        self.failUnlessEqual(atime2, atime)
        self.failUnlessEqual(mtime2, mtime)

    def test_write_dont_preserve_timestamps(self):
        stat = os.stat(self.pathname)
        atime = round(stat.st_atime)
        mtime = round(stat.st_mtime)
        metadata = ImageMetadata(self.pathname)
        metadata.read()
        metadata.comment = 'Yellow Submarine'
        time.sleep(1.1)
        metadata.write()
        stat2 = os.stat(self.pathname)
        atime2 = round(stat2.st_atime)
        mtime2 = round(stat2.st_mtime)
        # It is not safe to assume that atime will have been modified when the
        # file has been read, as it may depend on mount options (e.g. noatime,
        # relatime).
        # See discussion at http://bugs.launchpad.net/pyexiv2/+bug/624999.
        #self.failIfEqual(atime2, atime)
        self.failIfEqual(mtime2, mtime)
        metadata.comment = 'Yesterday'
        time.sleep(1.1)
        metadata.write(preserve_timestamps=True)
        stat3 = os.stat(self.pathname)
        atime3 = round(stat3.st_atime)
        mtime3 = round(stat3.st_mtime)
        self.failUnlessEqual(atime3, atime2)
        self.failUnlessEqual(mtime3, mtime2)

    ###########################
    # Test EXIF-related methods
    ###########################

    def test_exif_keys(self):
        self.metadata.read()
        self.assertEqual(self.metadata._keys['exif'], None)
        keys = self.metadata.exif_keys
        self.assertEqual(len(keys), 2)
        self.assertEqual(self.metadata._keys['exif'], keys)

    def test_get_exif_tag(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['exif'], {})
        # Get an existing tag
        key = 'Exif.Image.Make'
        tag = self.metadata._get_exif_tag(key)
        self.assert_(isinstance(tag, ExifTag))
        self.assertEqual(self.metadata._tags['exif'][key], tag)
        # Try to get an nonexistent tag
        key = 'Exif.Photo.Sharpness'
        self.failUnlessRaises(KeyError, self.metadata._get_exif_tag, key)

    def test_set_exif_tag_wrong(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['exif'], {})
        # Try to set a tag with wrong type
        tag = 'Not an exif tag'
        self.failUnlessRaises(TypeError, self.metadata._set_exif_tag, tag)
        self.assertEqual(self.metadata._tags['exif'], {})

    def test_set_exif_tag_create(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['exif'], {})
        # Create a new tag
        tag = ExifTag('Exif.Thumbnail.Orientation', 1)
        self.assert_(tag.key not in self.metadata.exif_keys)
        self.metadata._set_exif_tag(tag.key, tag)
        self.assert_(tag.key in self.metadata.exif_keys)
        self.assertEqual(self.metadata._tags['exif'], {tag.key: tag})
        self.assert_(tag.key in self.metadata._image._exifKeys())
        self.assertEqual(self.metadata._image._getExifTag(tag.key)._getRawValue(),
                         tag.raw_value)

    def test_set_exif_tag_overwrite(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['exif'], {})
        # Overwrite an existing tag
        tag = ExifTag('Exif.Image.DateTime', datetime.datetime(2009, 3, 20, 20, 32, 0))
        self.metadata._set_exif_tag(tag.key, tag)
        self.assertEqual(self.metadata._tags['exif'], {tag.key: tag})
        self.assert_(tag.key in self.metadata._image._exifKeys())
        self.assertEqual(self.metadata._image._getExifTag(tag.key)._getRawValue(),
                         tag.raw_value)

    def test_set_exif_tag_overwrite_already_cached(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['exif'], {})
        # Overwrite an existing tag already cached
        key = 'Exif.Image.Make'
        tag = self.metadata._get_exif_tag(key)
        self.assertEqual(self.metadata._tags['exif'][key], tag)
        new_tag = ExifTag(key, 'World Company')
        self.metadata._set_exif_tag(key, new_tag)
        self.assertEqual(self.metadata._tags['exif'], {key: new_tag})
        self.assert_(key in self.metadata._image._exifKeys())
        self.assertEqual(self.metadata._image._getExifTag(key)._getRawValue(),
                         new_tag.raw_value)

    def test_set_exif_tag_direct_value_assignment(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['exif'], {})
        # Direct value assignment: pass a value instead of a fully-formed tag
        key = 'Exif.Thumbnail.Orientation'
        value = 1
        self.metadata._set_exif_tag(key, value)
        self.assert_(key in self.metadata.exif_keys)
        self.assert_(key in self.metadata._image._exifKeys())
        tag = self.metadata._get_exif_tag(key)
        self.assertEqual(tag.value, value)
        self.assertEqual(self.metadata._tags['exif'], {key: tag})
        self.assertEqual(self.metadata._image._getExifTag(key)._getRawValue(),
                         tag.raw_value)

    def test_delete_exif_tag_inexistent(self):
        self.metadata.read()
        key = 'Exif.Image.Artist'
        self.failUnlessRaises(KeyError, self.metadata._delete_exif_tag, key)

    def test_delete_exif_tag_not_cached(self):
        self.metadata.read()
        key = 'Exif.Image.DateTime'
        self.assertEqual(self.metadata._tags['exif'], {})
        self.assert_(key in self.metadata.exif_keys)
        self.metadata._delete_exif_tag(key)
        self.assertEqual(self.metadata._tags['exif'], {})
        self.failIf(key in self.metadata.exif_keys)

    def test_delete_exif_tag_cached(self):
        self.metadata.read()
        key = 'Exif.Image.DateTime'
        self.assert_(key in self.metadata.exif_keys)
        tag = self.metadata._get_exif_tag(key)
        self.assertEqual(self.metadata._tags['exif'][key], tag)
        self.metadata._delete_exif_tag(key)
        self.assertEqual(self.metadata._tags['exif'], {})
        self.failIf(key in self.metadata.exif_keys)

    ###########################
    # Test IPTC-related methods
    ###########################

    def test_iptc_keys(self):
        self.metadata.read()
        self.assertEqual(self.metadata._keys['iptc'], None)
        keys = self.metadata.iptc_keys
        self.assertEqual(len(keys), 2)
        self.assertEqual(self.metadata._keys['iptc'], keys)

    def test_get_iptc_tag(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['iptc'], {})
        # Get an existing tag
        key = 'Iptc.Application2.DateCreated'
        tag = self.metadata._get_iptc_tag(key)
        self.assert_(isinstance(tag, IptcTag))
        self.assertEqual(self.metadata._tags['iptc'][key], tag)
        # Try to get an nonexistent tag
        key = 'Iptc.Application2.Copyright'
        self.failUnlessRaises(KeyError, self.metadata._get_iptc_tag, key)

    def test_set_iptc_tag_wrong(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['iptc'], {})
        # Try to set a tag with wrong type
        tag = 'Not an iptc tag'
        self.failUnlessRaises(TypeError, self.metadata._set_iptc_tag, tag)
        self.assertEqual(self.metadata._tags['iptc'], {})

    def test_set_iptc_tag_create(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['iptc'], {})
        # Create a new tag
        tag = IptcTag('Iptc.Application2.Writer', ['Nobody'])
        self.assert_(tag.key not in self.metadata.iptc_keys)
        self.metadata._set_iptc_tag(tag.key, tag)
        self.assert_(tag.key in self.metadata.iptc_keys)
        self.assertEqual(self.metadata._tags['iptc'], {tag.key: tag})
        self.assert_(tag.key in self.metadata._image._iptcKeys())
        self.assertEqual(self.metadata._image._getIptcTag(tag.key)._getRawValues(),
                         tag.raw_value)

    def test_set_iptc_tag_overwrite(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['iptc'], {})
        # Overwrite an existing tag
        tag = IptcTag('Iptc.Application2.Caption', ['A picture.'])
        self.metadata._set_iptc_tag(tag.key, tag)
        self.assertEqual(self.metadata._tags['iptc'], {tag.key: tag})
        self.assert_(tag.key in self.metadata._image._iptcKeys())
        self.assertEqual(self.metadata._image._getIptcTag(tag.key)._getRawValues(),
                         tag.raw_value)

    def test_set_iptc_tag_overwrite_already_cached(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['iptc'], {})
        # Overwrite an existing tag already cached
        key = 'Iptc.Application2.Caption'
        tag = self.metadata._get_iptc_tag(key)
        self.assertEqual(self.metadata._tags['iptc'][key], tag)
        new_tag = IptcTag(key, ['A picture.'])
        self.metadata._set_iptc_tag(key, new_tag)
        self.assertEqual(self.metadata._tags['iptc'], {key: new_tag})
        self.assert_(key in self.metadata._image._iptcKeys())
        self.assertEqual(self.metadata._image._getIptcTag(key)._getRawValues(),
                         new_tag.raw_value)

    def test_set_iptc_tag_direct_value_assignment(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['iptc'], {})
        # Direct value assignment: pass a value instead of a fully-formed tag
        key = 'Iptc.Application2.Writer'
        values = ['Nobody']
        self.metadata._set_iptc_tag(key, values)
        self.assert_(key in self.metadata.iptc_keys)
        self.assert_(key in self.metadata._image._iptcKeys())
        tag = self.metadata._get_iptc_tag(key)
        self.assertEqual(tag.value, values)
        self.assertEqual(self.metadata._tags['iptc'], {key: tag})
        self.assertEqual(self.metadata._image._getIptcTag(key)._getRawValues(),
                         tag.raw_value)

    def test_delete_iptc_tag_inexistent(self):
        self.metadata.read()
        key = 'Iptc.Application2.LocationCode'
        self.failUnlessRaises(KeyError, self.metadata._delete_iptc_tag, key)

    def test_delete_iptc_tag_not_cached(self):
        self.metadata.read()
        key = 'Iptc.Application2.Caption'
        self.assertEqual(self.metadata._tags['iptc'], {})
        self.assert_(key in self.metadata.iptc_keys)
        self.metadata._delete_iptc_tag(key)
        self.assertEqual(self.metadata._tags['iptc'], {})
        self.failIf(key in self.metadata.iptc_keys)

    def test_delete_iptc_tag_cached(self):
        self.metadata.read()
        key = 'Iptc.Application2.Caption'
        self.assert_(key in self.metadata.iptc_keys)
        tag = self.metadata._get_iptc_tag(key)
        self.assertEqual(self.metadata._tags['iptc'][key], tag)
        self.metadata._delete_iptc_tag(key)
        self.assertEqual(self.metadata._tags['iptc'], {})
        self.failIf(key in self.metadata.iptc_keys)

    ##########################
    # Test XMP-related methods
    ##########################

    def test_xmp_keys(self):
        self.metadata.read()
        self.assertEqual(self.metadata._keys['xmp'], None)
        keys = self.metadata.xmp_keys
        self.assertEqual(len(keys), 2)
        self.assertEqual(self.metadata._keys['xmp'], keys)

    def test_get_xmp_tag(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['xmp'], {})
        # Get an existing tag
        key = 'Xmp.dc.subject'
        tag = self.metadata._get_xmp_tag(key)
        self.assert_(isinstance(tag, XmpTag))
        self.assertEqual(self.metadata._tags['xmp'][key], tag)
        # Try to get an nonexistent tag
        key = 'Xmp.xmp.Label'
        self.failUnlessRaises(KeyError, self.metadata._get_xmp_tag, key)

    def test_set_xmp_tag_wrong(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['xmp'], {})
        # Try to set a tag with wrong type
        tag = 'Not an xmp tag'
        self.failUnlessRaises(TypeError, self.metadata._set_xmp_tag, tag)
        self.assertEqual(self.metadata._tags['xmp'], {})

    def test_set_xmp_tag_create(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['xmp'], {})
        # Create a new tag
        tag = XmpTag('Xmp.dc.title', {'x-default': 'This is not a title',
                                      'fr-FR': "Ceci n'est pas un titre"})
        self.assert_(tag.key not in self.metadata.xmp_keys)
        self.metadata._set_xmp_tag(tag.key, tag)
        self.assert_(tag.key in self.metadata.xmp_keys)
        self.assertEqual(self.metadata._tags['xmp'], {tag.key: tag})
        self.assert_(tag.key in self.metadata._image._xmpKeys())
        self.assertEqual(self.metadata._image._getXmpTag(tag.key)._getLangAltValue(),
                         tag.raw_value)

    def test_set_xmp_tag_overwrite(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['xmp'], {})
        # Overwrite an existing tag
        tag = XmpTag('Xmp.dc.format', ('image', 'png'))
        self.metadata._set_xmp_tag(tag.key, tag)
        self.assertEqual(self.metadata._tags['xmp'], {tag.key: tag})
        self.assert_(tag.key in self.metadata._image._xmpKeys())
        self.assertEqual(self.metadata._image._getXmpTag(tag.key)._getTextValue(),
                         tag.raw_value)

    def test_set_xmp_tag_overwrite_already_cached(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['xmp'], {})
        # Overwrite an existing tag already cached
        key = 'Xmp.dc.subject'
        tag = self.metadata._get_xmp_tag(key)
        self.assertEqual(self.metadata._tags['xmp'][key], tag)
        new_tag = XmpTag(key, ['hello', 'world'])
        self.metadata._set_xmp_tag(key, new_tag)
        self.assertEqual(self.metadata._tags['xmp'], {key: new_tag})
        self.assert_(key in self.metadata._image._xmpKeys())
        self.assertEqual(self.metadata._image._getXmpTag(key)._getArrayValue(),
                         new_tag.raw_value)

    def test_set_xmp_tag_direct_value_assignment(self):
        self.metadata.read()
        self.assertEqual(self.metadata._tags['xmp'], {})
        # Direct value assignment: pass a value instead of a fully-formed tag
        key = 'Xmp.dc.title'
        value = {'x-default': 'This is not a title',
                 'fr-FR': "Ceci n'est pas un titre"}
        self.metadata._set_xmp_tag(key, value)
        self.assert_(key in self.metadata.xmp_keys)
        self.assert_(key in self.metadata._image._xmpKeys())
        tag = self.metadata._get_xmp_tag(key)
        self.assertEqual(tag.value, value)
        self.assertEqual(self.metadata._tags['xmp'], {key: tag})
        self.assertEqual(self.metadata._image._getXmpTag(key)._getLangAltValue(), tag.raw_value)

    def test_delete_xmp_tag_inexistent(self):
        self.metadata.read()
        key = 'Xmp.xmp.CreatorTool'
        self.failUnlessRaises(KeyError, self.metadata._delete_xmp_tag, key)

    def test_delete_xmp_tag_not_cached(self):
        self.metadata.read()
        key = 'Xmp.dc.subject'
        self.assertEqual(self.metadata._tags['xmp'], {})
        self.assert_(key in self.metadata.xmp_keys)
        self.metadata._delete_xmp_tag(key)
        self.assertEqual(self.metadata._tags['xmp'], {})
        self.failIf(key in self.metadata.xmp_keys)

    def test_delete_xmp_tag_cached(self):
        self.metadata.read()
        key = 'Xmp.dc.subject'
        self.assert_(key in self.metadata.xmp_keys)
        tag = self.metadata._get_xmp_tag(key)
        self.assertEqual(self.metadata._tags['xmp'][key], tag)
        self.metadata._delete_xmp_tag(key)
        self.assertEqual(self.metadata._tags['xmp'], {})
        self.failIf(key in self.metadata.xmp_keys)

    ###########################
    # Test dictionary interface
    ###########################

    def test_getitem(self):
        self.metadata.read()
        # Get existing tags
        key = 'Exif.Image.DateTime'
        tag = self.metadata[key]
        self.assert_(isinstance(tag, ExifTag))
        key = 'Iptc.Application2.Caption'
        tag = self.metadata[key]
        self.assert_(isinstance(tag, IptcTag))
        key = 'Xmp.dc.format'
        tag = self.metadata[key]
        self.assert_(isinstance(tag, XmpTag))
        # Try to get nonexistent tags
        keys = ('Exif.Image.SamplesPerPixel', 'Iptc.Application2.FixtureId',
                'Xmp.xmp.Rating', 'Wrong.Noluck.Raise')
        for key in keys:
            self.failUnlessRaises(KeyError, self.metadata.__getitem__, key)

    def test_setitem(self):
        self.metadata.read()
        # Set new tags
        key = 'Exif.Photo.ExposureBiasValue'
        tag = ExifTag(key, make_fraction(0, 3))
        self.metadata[key] = tag
        self.failUnless(key in self.metadata._tags['exif'])
        self.failUnlessEqual(self.metadata._tags['exif'][key], tag)
        key = 'Iptc.Application2.City'
        tag = IptcTag(key, ['Barcelona'])
        self.metadata[key] = tag
        self.failUnless(key in self.metadata._tags['iptc'])
        self.failUnlessEqual(self.metadata._tags['iptc'][key], tag)
        key = 'Xmp.dc.description'
        tag = XmpTag(key, {'x-default': 'Sunset picture.'})
        self.metadata[key] = tag
        self.failUnless(key in self.metadata._tags['xmp'])
        self.failUnlessEqual(self.metadata._tags['xmp'][key], tag)
        # Replace existing tags
        key = 'Exif.Photo.ExifVersion'
        tag = ExifTag(key, '0220')
        self.metadata[key] = tag
        self.failUnless(key in self.metadata._tags['exif'])
        self.failUnlessEqual(self.metadata._tags['exif'][key], tag)
        key = 'Iptc.Application2.Caption'
        tag = IptcTag(key, ['Sunset on Barcelona.'])
        self.metadata[key] = tag
        self.failUnless(key in self.metadata._tags['iptc'])
        self.failUnlessEqual(self.metadata._tags['iptc'][key], tag)
        key = 'Xmp.dc.subject'
        tag = XmpTag(key, ['sunset', 'Barcelona', 'beautiful', 'beach'])
        self.metadata[key] = tag
        self.failUnless(key in self.metadata._tags['xmp'])
        self.failUnlessEqual(self.metadata._tags['xmp'][key], tag)

    def test_delitem(self):
        self.metadata.read()
        # Delete existing tags
        key = 'Exif.Image.Make'
        del self.metadata[key]
        self.failIf(key in self.metadata._keys['exif'])
        self.failIf(key in self.metadata._tags['exif'])
        key = 'Iptc.Application2.Caption'
        del self.metadata[key]
        self.failIf(key in self.metadata._keys['iptc'])
        self.failIf(key in self.metadata._tags['iptc'])
        key = 'Xmp.dc.subject'
        del self.metadata[key]
        self.failIf(key in self.metadata._keys['xmp'])
        self.failIf(key in self.metadata._tags['xmp'])
        # Try to delete nonexistent tags
        keys = ('Exif.Image.SamplesPerPixel', 'Iptc.Application2.FixtureId',
                'Xmp.xmp.Rating', 'Wrong.Noluck.Raise')
        for key in keys:
            self.failUnlessRaises(KeyError, self.metadata.__delitem__, key)

    def test_replace_tag_by_itself(self):
        # Test that replacing an existing tag by itself
        # doesn’t result in an ugly segmentation fault
        # (see https://bugs.launchpad.net/pyexiv2/+bug/622739).
        self.metadata.read()
        keys = self.metadata.exif_keys + \
               self.metadata.iptc_keys + \
               self.metadata.xmp_keys
        for key in keys:
            self.metadata[key] = self.metadata[key]        

    ##########################
    # Test the image comment #
    ##########################
    
    def test_get_comment(self):
        self.metadata.read()
        self.failUnlessEqual(self.metadata.comment, 'Hello World!')

    def test_set_comment(self):
        self.metadata.read()
        comment = 'Welcome to the real world.'
        self.metadata.comment = comment
        self.failUnlessEqual(self.metadata.comment, comment)
        self.metadata.comment = None
        self.failUnlessEqual(self.metadata.comment, '')

    def test_delete_comment(self):
        self.metadata.read()
        del self.metadata.comment
        self.failUnlessEqual(self.metadata.comment, '')

    ####################
    # Test metadata copy
    ####################

    def _set_up_other(self):
        self.other = ImageMetadata.from_buffer(EMPTY_JPG_DATA)

    def test_copy_metadata(self):
        self.metadata.read()
        self._set_up_other()
        self.other.read()
        families = ('exif', 'iptc', 'xmp')

        for family in families:
            self.failUnlessEqual(getattr(self.other, '%s_keys' % family), [])

        self.metadata.copy(self.other)

        for family in ('exif', 'iptc', 'xmp'):
            self.failUnlessEqual(self.other._keys[family], None)
            self.failUnlessEqual(self.other._tags[family], {})
            keys = getattr(self.metadata, '%s_keys' % family)
            self.failUnlessEqual(getattr(self.other._image, '_%sKeys' % family)(), keys)
            self.failUnlessEqual(getattr(self.other, '%s_keys' % family), keys)

        for key in self.metadata.exif_keys:
            self.failUnlessEqual(self.metadata[key].value, self.other[key].value)

        for key in self.metadata.iptc_keys:
            self.failUnlessEqual(self.metadata[key].value, self.other[key].value)

        for key in self.metadata.xmp_keys:
            self.failUnlessEqual(self.metadata[key].value, self.other[key].value)

        self.failUnlessEqual(self.metadata.comment, self.other.comment)

    #############################
    # Test MutableMapping methods
    #############################

    def _set_up_clean(self):
        self.clean = ImageMetadata.from_buffer(EMPTY_JPG_DATA)

    def test_mutablemapping(self):
        self._set_up_clean()
        self.clean.read()

        self.assertEqual(len(self.clean), 0)
        self.assertTrue('Exif.Image.DateTimeOriginal' not in self.clean)

        key = 'Exif.Image.DateTimeOriginal'
        correctDate = datetime.datetime(2007,03,11)
        incorrectDate = datetime.datetime(2009,03,25)
        tag_date = ExifTag(key,correctDate)
        false_tag_date = ExifTag(key,incorrectDate)
        self.clean[key] = tag_date

        self.assertEqual(len(self.clean), 1)
        self.assertTrue('Exif.Image.DateTimeOriginal' in self.clean)
        self.assertEqual(self.clean.get('Exif.Image.DateTimeOriginal', false_tag_date), tag_date)
        self.assertEqual(self.clean.get('Exif.Image.DateTime', tag_date), tag_date)

        key = 'Exif.Photo.UserComment'
        tag = ExifTag(key,'UserComment')
        self.clean[key] = tag        
        key = 'Iptc.Application2.Caption'
        tag = IptcTag(key,['Caption'])
        self.clean[key] = tag 
        key = 'Xmp.dc.subject'
        tag = XmpTag(key, ['subject', 'values'])
        self.clean[key] = tag

        self.assertTrue('Exif.Photo.UserComment' in self.clean)
        self.assertTrue('Iptc.Application2.Caption' in self.clean)
        self.assertTrue('Xmp.dc.subject' in self.clean)

        self.clean.clear()
        self.assertEqual(len(self.clean), 0)

        self.assertTrue('Exif.Photo.UserComment' not in self.clean)
        self.assertTrue('Iptc.Application2.Caption' not in self.clean)
        self.assertTrue('Xmp.dc.subject' not in self.clean)

    ###########################
    # Test the EXIF thumbnail #
    ###########################

    def _test_thumbnail_tags(self, there):
        keys = ('Exif.Thumbnail.Compression',
                'Exif.Thumbnail.JPEGInterchangeFormat',
                'Exif.Thumbnail.JPEGInterchangeFormatLength')
        for key in keys:
            self.assertEqual(key in self.metadata.exif_keys, there)

    def test_no_exif_thumbnail(self):
        self.metadata.read()
        thumb = self.metadata.exif_thumbnail
        self.assertEqual(thumb.mime_type, '')
        self.assertEqual(thumb.extension, '')
        self.assertEqual(thumb.data, '')
        self._test_thumbnail_tags(False)

    def test_set_exif_thumbnail_from_data(self):
        self.metadata.read()
        self._test_thumbnail_tags(False)
        thumb = self.metadata.exif_thumbnail
        thumb.data = EMPTY_JPG_DATA
        self.assertEqual(thumb.mime_type, 'image/jpeg')
        self.assertEqual(thumb.extension, '.jpg')
        self.assertEqual(thumb.data, EMPTY_JPG_DATA)
        self._test_thumbnail_tags(True)

    def test_set_exif_thumbnail_from_file(self):
        fd, pathname = tempfile.mkstemp(suffix='.jpg')
        os.write(fd, EMPTY_JPG_DATA)
        os.close(fd)
        self.metadata.read()
        self._test_thumbnail_tags(False)
        thumb = self.metadata.exif_thumbnail
        thumb.set_from_file(pathname)
        os.remove(pathname)
        self.assertEqual(thumb.mime_type, 'image/jpeg')
        self.assertEqual(thumb.extension, '.jpg')
        self.assertEqual(thumb.data, EMPTY_JPG_DATA)
        self._test_thumbnail_tags(True)

    def test_write_exif_thumbnail_to_file(self):
        self.metadata.read()
        self._test_thumbnail_tags(False)
        thumb = self.metadata.exif_thumbnail
        thumb.data = EMPTY_JPG_DATA
        fd, pathname = tempfile.mkstemp()
        os.close(fd)
        os.remove(pathname)
        thumb.write_to_file(pathname)
        pathname = pathname + thumb.extension
        fd = open(pathname, 'rb')
        self.assertEqual(fd.read(), EMPTY_JPG_DATA)
        fd.close()
        os.remove(pathname)

    def test_erase_exif_thumbnail(self):
        self.metadata.read()
        self._test_thumbnail_tags(False)
        thumb = self.metadata.exif_thumbnail
        thumb.data = EMPTY_JPG_DATA
        self.assertEqual(thumb.mime_type, 'image/jpeg')
        self.assertEqual(thumb.extension, '.jpg')
        self.assertEqual(thumb.data, EMPTY_JPG_DATA)
        self._test_thumbnail_tags(True)
        thumb.erase()
        self.assertEqual(thumb.mime_type, '')
        self.assertEqual(thumb.extension, '')
        self.assertEqual(thumb.data, '')
        self._test_thumbnail_tags(False)

    def test_set_exif_thumbnail_from_invalid_data(self):
        # No check on the format of the buffer is performed, therefore it will
        # always work.
        self.metadata.read()
        self._test_thumbnail_tags(False)
        thumb = self.metadata.exif_thumbnail
        thumb.data = 'invalid'
        self.assertEqual(thumb.mime_type, 'image/jpeg')
        self._test_thumbnail_tags(True)

    def test_set_exif_thumbnail_from_inexistent_file(self):
        self.metadata.read()
        self._test_thumbnail_tags(False)
        thumb = self.metadata.exif_thumbnail
        fd, pathname = tempfile.mkstemp()
        os.close(fd)
        os.remove(pathname)
        self.failUnlessRaises(IOError, thumb.set_from_file, pathname)
        self._test_thumbnail_tags(False)

    def test_exif_thumbnail_is_preview(self):
        self.metadata.read()
        self._test_thumbnail_tags(False)
        self.assertEqual(len(self.metadata.previews), 0)
        thumb = self.metadata.exif_thumbnail
        thumb.data = EMPTY_JPG_DATA
        self._test_thumbnail_tags(True)
        self.assertEqual(len(self.metadata.previews), 1)
        preview = self.metadata.previews[0]
        self.assertEqual(thumb.mime_type, preview.mime_type)
        self.assertEqual(thumb.extension, preview.extension)
        self.assertEqual(thumb.data, preview.data)

    #########################
    # Test the IPTC charset #
    #########################

    def test_guess_iptc_charset(self):
        # If no charset is defined, exiv2 guesses it from the encoding of the
        # metadata.
        self.metadata.read()
        self.assertEqual(self.metadata.iptc_charset, 'ascii')
        self.metadata['Iptc.Application2.City'] = [u'Córdoba']
        self.assertEqual(self.metadata.iptc_charset, 'utf-8')

    def test_set_iptc_charset_utf8(self):
        self.metadata.read()
        self.assert_('Iptc.Envelope.CharacterSet' not in self.metadata.iptc_keys)
        self.assertEqual(self.metadata.iptc_charset, 'ascii')
        values = ('utf-8', 'utf8', 'u8', 'utf', 'utf_8')
        for value in values:
            self.metadata.iptc_charset = value
            self.assertEqual(self.metadata.iptc_charset, 'utf-8')
            self.metadata.iptc_charset = value.upper()
            self.assertEqual(self.metadata.iptc_charset, 'utf-8')

    def test_set_invalid_iptc_charset(self):
        self.metadata.read()
        self.assert_('Iptc.Envelope.CharacterSet' not in self.metadata.iptc_keys)
        values = ('invalid', 'utf-9', '3.14')
        for value in values:
            self.assertRaises(ValueError, self.metadata.__setattr__,
                              'iptc_charset', value)

    def test_set_unhandled_iptc_charset(self):
        # At the moment, the only charset handled is UTF-8.
        self.metadata.read()
        self.assert_('Iptc.Envelope.CharacterSet' not in self.metadata.iptc_keys)
        values = ('ascii', 'iso8859_15', 'shift_jis')
        for value in values:
            self.assertRaises(ValueError, self.metadata.__setattr__,
                              'iptc_charset', value)

    def test_delete_iptc_charset(self):
        self.metadata.read()
        key = 'Iptc.Envelope.CharacterSet'

        self.assertEqual(self.metadata.iptc_charset, 'ascii')
        self.assert_(key not in self.metadata.iptc_keys)
        del self.metadata.iptc_charset
        self.assertEqual(self.metadata.iptc_charset, 'ascii')
        self.assert_(key not in self.metadata.iptc_keys)

        self.metadata.iptc_charset = 'utf-8'
        self.assertEqual(self.metadata.iptc_charset, 'utf-8')
        self.assert_(key in self.metadata.iptc_keys)
        del self.metadata.iptc_charset
        self.assertEqual(self.metadata.iptc_charset, 'ascii')
        self.assert_(key not in self.metadata.iptc_keys)

        self.metadata.iptc_charset = 'utf-8'
        self.assertEqual(self.metadata.iptc_charset, 'utf-8')
        self.assert_(key in self.metadata.iptc_keys)
        self.metadata.iptc_charset = None
        self.assertEqual(self.metadata.iptc_charset, 'ascii')
        self.assert_(key not in self.metadata.iptc_keys)