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
3 years ago
Changed (Δ550 bytes):
raw changeset »
iptcinfo.py (84 lines added, 77 lines removed)
setup.py (4 lines added, 2 lines removed)
1 |
#!/ |
|
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__ = |
|
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 __de |
|
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, |
|
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 |
|
|
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 = |
|
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 |
|
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 |
|
641 |
out.append( P("<keywords>") ) |
|
640 |
642 |
off += 1 |
641 |
for k in kw: out |
|
643 |
for k in kw: out.append( P("<keyword>%s</keyword>" % k) ) |
|
642 |
644 |
off -= 1 |
643 |
out |
|
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 |
|
650 |
out.append( P("<supplemental_categories>") ) |
|
649 |
651 |
off += 1 |
650 |
652 |
for k in sc: |
651 |
out |
|
653 |
out.append( P("<supplemental_category>%s</supplemental_category>" % k) ) |
|
652 |
654 |
off -= 1 |
653 |
out |
|
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 |
|
660 |
out.append( P("<contacts>") ) |
|
659 |
661 |
off += 1 |
660 |
for k in kw: out |
|
662 |
for k in kw: out.append( P("<contact>%s</contact>" % k) ) |
|
661 |
663 |
off -= 1 |
662 |
out |
|
664 |
out.append( P("</contacts>") ) |
|
663 |
665 |
|
664 |
666 |
# close base tag |
665 |
667 |
off -= 1 |
666 |
out |
|
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 |
|
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. |
|
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] |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
1050 |
end.append(buff) |
|
1049 |
1051 |
|
1050 |
return ( |
|
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 |
|
|
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 |
|
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 |
|
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 |
|
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 |
|
1168 |
resourceBlock.append( "8BIM" ) |
|
1164 |
1169 |
# 0x0404 is IIM data, 00 is required empty string |
1165 |
resourceBlock |
|
1170 |
resourceBlock.append( pack("BBBB", 0x04, 0x04, 0, 0) ) |
|
1166 |
1171 |
# length of data as 32-bit, network-byte order |
1167 |
resourceBlock |
|
1172 |
resourceBlock.append( pack("!L", len(data)) ) |
|
1168 |
1173 |
# Now tack data on there |
1169 |
resourceBlock |
|
1174 |
resourceBlock.append( data ) |
|
1170 |
1175 |
# Pad with a blank if not even size |
1171 |
if len(data) % 2 != 0: resourceBlock |
|
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 |
|
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 |
|
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 |
|
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 |
|
|
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.""" |
| … | … | @@ -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', |
