gthomas / iptcinfo

A Python port of Josh Carter's IPTCInfo (Perl)

Clone this repository (size: 182.2 KB): HTTPS / SSH
$ hg clone http://bitbucket.org/gthomas/iptcinfo/
commit 6: 1b29e2331311
parent 5: 793eacaf88fa
branch: default
+= -> [].append() and a few pychecker cleanings
Tamás Gulácsi
3 years ago

Changed (Δ550 bytes):

raw changeset »

iptcinfo.py (84 lines added, 77 lines removed)

setup.py (4 lines added, 2 lines removed)

Up to file-list iptcinfo.py:

1
#!/bin/userenv python
1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
2
3
# vim: mode=python fenc=utf-8 fileformat=unix:
3
# -*- coding: utf-8 -*-
4
4
# Author: 2004-2006 Gulácsi Tamás
5
5
#
6
6
# Ported from Josh Carter's Perl IPTCInfo.pm by Tam?s Gul?csi
13
13
# it under the same terms as Python itself.
14
14
#
15
15
# VERSION = '1.9';
16
"""
16
u"""
17
17
IPTCInfo - Python module for extracting and modifying IPTC image meta-data
18
18
19
19
Ported from Josh Carter's Perl IPTCInfo-1.9.pm by Tamás Gulácsi
@@ -237,7 +237,7 @@ Josh Carter, josh@multipart-mixed.com
237
237
"""
238
238
239
239
__version__ = '1.9.2-rc7'
240
__author__ = 'Gulácsi, Tamás'
240
__author__ = u'Gulácsi, Tamás'
241
241
242
242
SURELY_WRITE_CHARSET_INFO = False
243
243
@@ -350,6 +350,7 @@ c_datasets = {
350
350
}
351
351
352
352
c_datasets_r = dict([(v, k) for k, v in c_datasets.iteritems()])
353
del k, v
353
354
354
355
class IPTCData(dict):
355
356
  """Dict with int/string keys from c_listdatanames"""
@@ -549,7 +550,7 @@ class IPTCInfo(object):
549
550
    fh2.close()
550
551
    return True
551
552
552
  def __destroy__(self):
553
  def __del__(self):
553
554
    """Called when object is destroyed. No action necessary in this case."""
554
555
    pass
555
556
@@ -560,7 +561,7 @@ class IPTCInfo(object):
560
561
561
562
  def getData(self):
562
563
    return self._data
563
  def setData(self, value):
564
  def setData(self, _):
564
565
    raise Exception('You cannot overwrite the data, only its elements!')
565
566
  data = property(getData, setData)
566
567
@@ -615,55 +616,56 @@ class IPTCInfo(object):
615
616
    filename, and the XML will be dumped into there."""
616
617
617
618
    def P(s):
618
      global off
619
      #global off
619
620
      return '  '*off + s + '\n'
620
621
    off = 0
621
622
622
623
    if len(basetag) == 0: basetag = 'photo'
623
    out = P("<%s>" % basetag)
624
    out = [P("<%s>" % basetag)]
624
625
625
626
    off += 1
626
627
    # dump extra info first, if any
627
    for k, v in (isinstance(extra, dict) and [extra] or [{}])[0].iteritems():
628
      out += P("<%s>%s</%s>" % (k, v, k))
628
    for k, v in (isinstance(extra, dict) 
629
        and [extra] or [{}])[0].iteritems():
630
      out.append( P("<%s>%s</%s>" % (k, v, k)) )
629
631
630
632
    # dump our stuff
631
633
    for k, v in self._data.iteritems():
632
634
      if not isinstance(v, list):
633
635
        key = re.sub('/', '-', re.sub(' +', ' ', self._data.keyAsStr(k)))
634
        out += P("<%s>%s</%s>" % (key, v, key))
636
        out.append( P("<%s>%s</%s>" % (key, v, key)) )
635
637
636
638
    # print keywords
637
639
    kw = self.keywords()
638
640
    if kw and len(kw) > 0:
639
      out += P("<keywords>")
641
      out.append( P("<keywords>") )
640
642
      off += 1
641
      for k in kw: out += P("<keyword>%s</keyword>" % k)
643
      for k in kw: out.append( P("<keyword>%s</keyword>" % k) )
642
644
      off -= 1
643
      out += P("</keywords>")
645
      out.append( P("</keywords>") )
644
646
645
647
    # print supplemental categories
646
648
    sc = self.supplementalCategories()
647
649
    if sc and len(sc) > 0:
648
      out += P("<supplemental_categories>")
650
      out.append( P("<supplemental_categories>") )
649
651
      off += 1
650
652
      for k in sc:
651
        out += P("<supplemental_category>%s</supplemental_category>" % k)
653
        out.append( P("<supplemental_category>%s</supplemental_category>" % k) )
652
654
      off -= 1
653
      out += P("</supplemental_categories>")
655
      out.append( P("</supplemental_categories>") )
654
656
655
657
    # print contacts
656
658
    kw = self.contacts()
657
659
    if kw and len(kw) > 0:
658
      out += P("<contacts>")
660
      out.append( P("<contacts>") )
659
661
      off += 1
660
      for k in kw: out += P("<contact>%s</contact>" % k)
662
      for k in kw: out.append( P("<contact>%s</contact>" % k) )
661
663
      off -= 1
662
      out += P("</contacts>")
664
      out.append( P("</contacts>") )
663
665
664
666
    # close base tag
665
667
    off -= 1
666
    out += P("</%s>" % basetag)
668
    out.append( P("</%s>" % basetag) )
667
669
668
670
    # export to file if caller asked for it.
669
671
    if len(filename) > 0:
@@ -671,7 +673,7 @@ class IPTCInfo(object):
671
673
      xmlout.write(out)
672
674
      xmlout.close()
673
675
674
    return out
676
    return ''.join(out)
675
677
676
678
  def exportSQL(self, tablename, mappings, extra):
677
679
    """statement = info.exportSQL('mytable', mappings, extra-data)
@@ -697,7 +699,7 @@ class IPTCInfo(object):
697
699
    # start with extra data, if any
698
700
    columns = ', '.join(extra.keys() + mappings.keys())
699
701
    values = ', '.join(map(E, extra.values()
700
                           + [self.getData(k) for k in mappings.keys()]))
702
                           + [self.data[k] for k in mappings.keys()]))
701
703
    # process our data
702
704
703
705
    statement = "INSERT INTO %s (%s) VALUES (%s)" \
@@ -942,7 +944,7 @@ class IPTCInfo(object):
942
944
      # and, if unsuccessful, into _data. Tags which are not in the
943
945
      # current IIM spec (version 4) are currently discarded.
944
946
      if self._data.has_key(dataset) and isinstance(self._data[dataset], list):
945
        self._data[dataset] += [value]
947
        self._data[dataset].append( value )
946
948
      elif dataset != 0:
947
949
        self._data[dataset] = value
948
950
@@ -959,7 +961,7 @@ class IPTCInfo(object):
959
961
    ## assert isinstance(fh, file)
960
962
    assert duck_typed(fh, ['seek', 'read'])
961
963
    adobeParts = ''
962
    start = ''
964
    start = []
963
965
964
966
    # Start at beginning of file
965
967
    fh.seek(0, 0)
@@ -971,7 +973,7 @@ class IPTCInfo(object):
971
973
      return None
972
974
973
975
    # Begin building start of file
974
    start += pack("BB", 0xff, 0xd8)
976
    start.append( pack("BB", 0xff, 0xd8) )
975
977
976
978
    # Get first marker in file. This will be APP0 for JFIF or APP1 for
977
979
    # EXIF.
@@ -985,23 +987,23 @@ class IPTCInfo(object):
985
987
986
988
    if ord(marker) == 0xe0 or not discardAppParts:
987
989
      # Always include APP0 marker at start if it's present.
988
      start += pack('BB', 0xff, ord(marker))
990
      start.append( pack('BB', 0xff, ord(marker)) )
989
991
      # Remember that the length must include itself (2 bytes)
990
      start += pack('!H', len(app0data)+2)
991
      start += app0data
992
      start.append( pack('!H', len(app0data)+2) )
993
      start.append( app0data )
992
994
    else:
993
995
      # Manually insert APP0 if we're trashing application parts, since
994
996
      # all JFIF format images should start with the version block.
995
997
      debug(2, 'discardAppParts=', discardAppParts)
996
      start += pack("BB", 0xff, 0xe0)
997
      start += pack("!H", 16)    # length (including these 2 bytes)
998
      start += "JFIF"            # format
999
      start += pack("BB", 1, 2)  # call it version 1.2 (current JFIF)
1000
      start += pack('8B', 0)     # zero everything else
998
      start.append( pack("BB", 0xff, 0xe0) )
999
      start.append( pack("!H", 16) )  # length (including these 2 bytes)
1000
      start.append( "JFIF" )          # format
1001
      start.append( pack("BB", 1, 2) )# call it version 1.2 (current JFIF)
1002
      start.append( pack('8B', 0) )   # zero everything else
1001
1003
1002
1004
    # Now scan through all markers in file until we hit image data or
1003
1005
    # IPTC stuff.
1004
    end = ''
1006
    end = []
1005
1007
    while 1:
1006
1008
      marker = self.jpegNextMarker(fh)
1007
1009
      if marker is None or ord(marker) == 0:
@@ -1011,12 +1013,12 @@ class IPTCInfo(object):
1011
1013
      # Check for end of image
1012
1014
      elif ord(marker) == 0xd9:
1013
1015
        self.log("JpegCollectFileParts: saw end of image marker")
1014
        end += pack("BB", 0xff, ord(marker))
1016
        end.append( pack("BB", 0xff, ord(marker)) )
1015
1017
        break
1016
1018
      # Check for start of compressed data
1017
1019
      elif ord(marker) == 0xda:
1018
1020
        self.log("JpegCollectFileParts: saw start of compressed data")
1019
        end += pack("BB", 0xff, ord(marker))
1021
        end.append( pack("BB", 0xff, ord(marker)) )
1020
1022
        break
1021
1023
      partdata = ''
1022
1024
      partdata = self.jpegSkipVariable(fh, partdata)
@@ -1037,17 +1039,17 @@ class IPTCInfo(object):
1037
1039
        break
1038
1040
      else:
1039
1041
        # Append all other parts to start section
1040
        start += pack("BB", 0xff, ord(marker))
1041
        start += pack("!H", len(partdata) + 2)
1042
        start += partdata
1042
        start.append( pack("BB", 0xff, ord(marker)) )
1043
        start.append( pack("!H", len(partdata) + 2) )
1044
        start.append( partdata )
1043
1045
1044
1046
    # Append rest of file to end
1045
1047
    while 1:
1046
      buff = fh.read()
1048
      buff = fh.read(8192)
1047
1049
      if buff is None or len(buff) == 0: break
1048
      end += buff
1050
      end.append(buff)
1049
1051
1050
    return (start, end, adobeParts)
1052
    return (''.join(start), ''.join(end), adobeParts)
1051
1053
1052
1054
  def collectAdobeParts(self, data):
1053
1055
    """Part APP13 contains yet another markup format, one defined by
@@ -1059,7 +1061,7 @@ class IPTCInfo(object):
1059
1061
    assert isinstance(data, basestring)
1060
1062
    length = len(data)
1061
1063
    offset = 0
1062
    out = ''
1064
    out = []
1063
1065
    # Skip preamble
1064
1066
    offset = len('Photoshop 3.0 ')
1065
1067
    # Process everything
@@ -1093,15 +1095,18 @@ class IPTCInfo(object):
1093
1095
1094
1096
      # skip IIM data (0x0404), but write everything else out
1095
1097
      if not (id1 == 4 and id2 == 4):
1096
        out += pack("!LBB", ostype, id1, id2)
1097
        out += pack("B", stringlen)
1098
        out += string
1099
        if stringlen == 0 or stringlen % 2 != 0: out += pack("B", 0)
1100
        out += pack("!L", size)
1101
        out += var
1102
        if size % 2 != 0 and len(out) % 2 != 0: out += pack("B", 0)
1098
        out.append( pack("!LBB", ostype, id1, id2) )
1099
        out.append( pack("B", stringlen) )
1100
        out.append( string )
1101
        if stringlen == 0 or stringlen % 2 != 0: 
1102
          out.append( pack("B", 0) )
1103
        out.append( pack("!L", size) )
1104
        out.append( var )
1105
        out = [''.join(out)]
1106
        if size % 2 != 0 and len(out[0]) % 2 != 0: 
1107
          out.append( pack("B", 0) )
1103
1108
1104
    return out
1109
    return ''.join(out)
1105
1110
1106
1111
  def _enc(self, text):
1107
1112
    """Recodes the given text from the old character set to utf-8"""
@@ -1123,11 +1128,11 @@ class IPTCInfo(object):
1123
1128
  def packedIIMData(self):
1124
1129
    """Assembles and returns our _data and _listdata into IIM format for
1125
1130
    embedding into an image."""
1126
    out = ''
1131
    out = []
1127
1132
    (tag, record) = (0x1c, 0x02)
1128
1133
    # Print record version
1129
1134
    # tag - record - dataset - len (short) - 4 (short)
1130
    out += pack("!BBBHH", tag, record, 0, 2, 4)
1135
    out.append( pack("!BBBHH", tag, record, 0, 2, 4) )
1131
1136
1132
1137
    debug(3, self.hexDump(out))
1133
1138
    # Iterate over data sets
@@ -1141,42 +1146,42 @@ class IPTCInfo(object):
1141
1146
      print value
1142
1147
      if not isinstance(value, list):
1143
1148
        value = str(value)
1144
        out += pack("!BBBH", tag, record, dataset, len(value))
1145
        out += value
1149
        out.append( pack("!BBBH", tag, record, dataset, len(value)) )
1150
        out.append( value )
1146
1151
      else:
1147
1152
        for v in map(str, value):
1148
1153
          if v is None or len(v) == 0: continue
1149
          out += pack("!BBBH", tag, record, dataset, len(v))
1150
          out += v
1154
          out.append( pack("!BBBH", tag, record, dataset, len(v)) )
1155
          out.append( v )
1151
1156
1152
    return out
1157
    return ''.join(out)
1153
1158
1154
1159
  def photoshopIIMBlock(self, otherparts, data):
1155
1160
    """Assembles the blob of Photoshop "resource data" that includes our
1156
1161
    fresh IIM data (from PackedIIMData) and the other Adobe parts we
1157
1162
    found in the file, if there were any."""
1158
    out = ''
1163
    out = []
1159
1164
    assert isinstance(data, basestring)
1160
    resourceBlock = "Photoshop 3.0"
1161
    resourceBlock += pack("B", 0)
1165
    resourceBlock = ["Photoshop 3.0"]
1166
    resourceBlock.append( pack("B", 0) )
1162
1167
    # Photoshop identifier
1163
    resourceBlock += "8BIM"
1168
    resourceBlock.append( "8BIM" )
1164
1169
    # 0x0404 is IIM data, 00 is required empty string
1165
    resourceBlock += pack("BBBB", 0x04, 0x04, 0, 0)
1170
    resourceBlock.append( pack("BBBB", 0x04, 0x04, 0, 0) )
1166
1171
    # length of data as 32-bit, network-byte order
1167
    resourceBlock += pack("!L", len(data))
1172
    resourceBlock.append( pack("!L", len(data)) )
1168
1173
    # Now tack data on there
1169
    resourceBlock += data
1174
    resourceBlock.append( data )
1170
1175
    # Pad with a blank if not even size
1171
    if len(data) % 2 != 0: resourceBlock += pack("B", 0)
1176
    if len(data) % 2 != 0: resourceBlock.append( pack("B", 0) )
1172
1177
    # Finally tack on other data
1173
    if otherparts is not None: resourceBlock += otherparts
1178
    if otherparts is not None: resourceBlock.append( otherparts )
1174
1179
1175
    out += pack("BB", 0xff, 0xed) # Jpeg start of block, APP13
1176
    out += pack("!H", len(resourceBlock) + 2) # length
1177
    out += resourceBlock
1180
    out.append( pack("BB", 0xff, 0xed) ) # Jpeg start of block, APP13
1181
    out.append( pack("!H", len(resourceBlock) + 2) ) # length
1182
    out.extend( resourceBlock )
1178
1183
1179
    return out
1184
    return ''.join(out)
1180
1185
1181
1186
  #######################################################################
1182
1187
  # Helpers, docs
@@ -1192,12 +1197,14 @@ class IPTCInfo(object):
1192
1197
    length  = len(dump)
1193
1198
    P = lambda z: ((ord(z) >= 0x21 and ord(z) <= 0x7e) and [z] or ['.'])[0]
1194
1199
    ROWLEN = 18
1195
    ered = '\n'
1196
    for j in range(0, length/ROWLEN + int(length%ROWLEN>0)):
1200
    ered = ['\n']
1201
    for j in range(0, length//ROWLEN + int(length%ROWLEN>0)):
1197
1202
      row = dump[j*ROWLEN:(j+1)*ROWLEN]
1198
      ered += ('%02X '*len(row) + '   '*(ROWLEN-len(row)) + '| %s\n') % \
1199
              tuple(map(ord, row) + [''.join(map(P, row))])
1200
    return ered
1203
      ered.append( 
1204
      ('%02X '*len(row) + '   '*(ROWLEN-len(row)) + '| %s\n') % \
1205
        tuple(map(ord, row) + [''.join(map(P, row))]) 
1206
        )
1207
    return ''.join(ered)
1201
1208
1202
1209
  def jpegDebugScan(self, filename):
1203
1210
    """Also very helpful when debugging."""

Up to file-list setup.py:

@@ -125,8 +125,10 @@ version = '1.9.2-rc7'
125
125
zipext = (sys.platform.startswith('Win') and ['zip'] or ['tar.gz'])[0]
126
126
setup(name='IPTCInfo',
127
127
      version=version,
128
      #url='http://www.fw.hu/gthomas/python/IPTCInfo-%s.%s' % (version, zipext),
129
      url='http://gthomas.homelinux.org/python/IPTCInfo-%s.%s' % (version, zipext),
128
      url='http://gthomas.homelinux.org/hg/iptcinfo/file/',
129
      download_url='http://gthomas.homelinux.org/python/IPTCInfo-%s.%s' % (version, zipext),
130
      author=u'Tamás Gulácsi',
131
      author_email='gthomas@fw.hu',
130
132
      maintainer=u'Tamás Gulácsi',
131
133
      maintainer_email='gthomas@fw.hu',
132
134
      license = 'http://www.opensource.org/licenses/gpl-license.php',