123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649 |
- #!/usr/bin/env python
- # This file is part of psdparser.
- #
- # Psdparser 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 3 of the License, or
- # (at your option) any later version.
- #
- # Psdparser 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 Psdparser. If not, see <http://www.gnu.org/licenses/>.
- import logging
- import sys
- from struct import unpack, calcsize
- from PIL import Image
- logger = logging.getLogger(__name__)
- """
- Header mode field meanings
- """
- CHANNEL_SUFFIXES = {
- -2: 'layer mask',
- -1: 'A',
- 0: 'R',
- 1: 'G',
- 2: 'B',
- 3: 'RGB',
- 4: 'CMYK', 5: 'HSL', 6: 'HSB',
- 9: 'Lab', 11: 'RGB',
- 12: 'Lab', 13: 'CMYK',
- }
- """
- Resource id descriptions
- """
- RESOURCE_DESCRIPTIONS = {
- 1000: 'PS2.0 mode data',
- 1001: 'Macintosh print record',
- 1003: 'PS2.0 indexed color table',
- 1005: 'ResolutionInfo',
- 1006: 'Names of the alpha channels',
- 1007: 'DisplayInfo',
- 1008: 'Caption',
- 1009: 'Border information',
- 1010: 'Background color',
- 1011: 'Print flags',
- 1012: 'Grayscale/multichannel halftoning info',
- 1013: 'Color halftoning info',
- 1014: 'Duotone halftoning info',
- 1015: 'Grayscale/multichannel transfer function',
- 1016: 'Color transfer functions',
- 1017: 'Duotone transfer functions',
- 1018: 'Duotone image info',
- 1019: 'B&W values for the dot range',
- 1021: 'EPS options',
- 1022: 'Quick Mask info',
- 1024: 'Layer state info',
- 1025: 'Working path',
- 1026: 'Layers group info',
- 1028: 'IPTC-NAA record (File Info)',
- 1029: 'Image mode for raw format files',
- 1030: 'JPEG quality',
- 1032: 'Grid and guides info',
- 1033: 'Thumbnail resource',
- 1034: 'Copyright flag',
- 1035: 'URL',
- 1036: 'Thumbnail resource',
- 1037: 'Global Angle',
- 1038: 'Color samplers resource',
- 1039: 'ICC Profile',
- 1040: 'Watermark',
- 1041: 'ICC Untagged',
- 1042: 'Effects visible',
- 1043: 'Spot Halftone',
- 1044: 'Document specific IDs',
- 1045: 'Unicode Alpha Names',
- 1046: 'Indexed Color Table Count',
- 1047: 'Transparent Index',
- 1049: 'Global Altitude',
- 1050: 'Slices',
- 1051: 'Workflow URL',
- 1052: 'Jump To XPEP',
- 1053: 'Alpha Identifiers',
- 1054: 'URL List',
- 1057: 'Version Info',
- 2999: 'Name of clipping path',
- 10000: 'Print flags info',
- }
- MODES = {
- 0: 'Bitmap',
- 1: 'GrayScale',
- 2: 'IndexedColor',
- 3: 'RGBColor',
- 4: 'CMYKColor',
- 5: 'HSLColor',
- 6: 'HSBColor',
- 7: 'Multichannel',
- 8: 'Duotone',
- 9: 'LabColor',
- 10: 'Gray16',
- 11: 'RGB48',
- 12: 'Lab48',
- 13: 'CMYK64',
- 14: 'DeepMultichannel',
- 15: 'Duotone16',
- }
- COMPRESSIONS = {
- 0: 'Raw',
- 1: 'RLE',
- 2: 'ZIP',
- 3: 'ZIPPrediction',
- }
- BLENDINGS = {
- 'norm': 'normal',
- 'dark': 'darken',
- 'mul ': 'multiply',
- 'lite': 'lighten',
- 'scrn': 'screen',
- 'over': 'overlay',
- 'sLit': 'soft-light',
- 'hLit': 'hard-light',
- 'lLit': 'linear-light',
- 'diff': 'difference',
- 'smud': 'exclusion',
- }
- PIL_BANDS = {
- 'R': 0,
- 'G': 1,
- 'B': 2,
- 'A': 3,
- 'L': 0,
- }
- def INDENT_OUTPUT(depth, msg):
- return ''.join([' ' for i in range(0, depth)]) + msg
- class PSDParser(object):
- header = None
- ressources = None
- num_layers = 0
- layers = None
- images = None
- merged_image = None
- def __init__(self, filename):
- self.filename = filename
- def _pad2(self, i):
- """same or next even"""
- return (i + 1) / 2 * 2
- def _pad4(self, i):
- """same or next multiple of 4"""
- return (i + 3) / 4 * 4
- def _readf(self, format):
- """read a strct from file structure according to format"""
- return unpack(format, self.fd.read(calcsize(format)))
- def _skip_block(self, desc, indent=0, new_line=False):
- (n,) = self._readf('>L') # (n,) is a 1-tuple.
- if n:
- self.fd.seek(n, 1) # 1: relative
- if new_line:
- logger.debug('')
- logger.debug(INDENT_OUTPUT(indent, 'Skipped %s with %s bytes' % (desc, n)))
- def parse(self):
- logger.debug("Opening '%s'" % self.filename)
- self.fd = open(self.filename, 'rb')
- try:
- self.parse_header()
- self.parse_image_resources()
- self.parse_layers_and_masks()
- self.parse_image_data()
- finally:
- self.fd.close()
- logger.debug("")
- logger.debug("DONE")
- def parse_header(self):
- logger.debug("")
- logger.debug("# Header #")
- self.header = {}
- C_PSD_HEADER = ">4sH 6B HLLHH"
- (
- self.header['sig'],
- self.header['version'],
- self.header['r0'],
- self.header['r1'],
- self.header['r2'],
- self.header['r3'],
- self.header['r4'],
- self.header['r5'],
- self.header['channels'],
- self.header['rows'],
- self.header['cols'],
- self.header['depth'],
- self.header['mode']
- ) = self._readf(C_PSD_HEADER)
- self.size = [self.header['rows'], self.header['cols']]
- if self.header['sig'] != "8BPS":
- raise ValueError("Not a PSD signature: '%s'" % self.header['sig'])
- if self.header['version'] != 1:
- raise ValueError("Can not handle PSD version:%d" % self.header['version'])
- self.header['modename'] = MODES[self.header['mode']] if 0 <= self.header['mode'] < 16 else "(%s)" % self.header['mode']
- logger.debug(INDENT_OUTPUT(1, "channels:%(channels)d, rows:%(rows)d, cols:%(cols)d, depth:%(depth)d, mode:%(mode)d [%(modename)s]" % self.header))
- logger.debug(INDENT_OUTPUT(1, "%s" % self.header))
- # Remember position
- self.header['colormodepos'] = self.fd.tell()
- self._skip_block("color mode data", 1)
- def parse_image_resources(self):
- def parse_irb():
- """return total bytes in block"""
- r = {}
- r['at'] = self.fd.tell()
- (r['type'], r['id'], r['namelen']) = self._readf(">4s H B")
- n = self._pad2(r['namelen'] + 1) - 1
- (r['name'],) = self._readf(">%ds" % n)
- r['name'] = r['name'][:-1] # skim off trailing 0byte
- r['short'] = r['name'][:20]
- (r['size'],) = self._readf(">L")
- self.fd.seek(self._pad2(r['size']), 1) # 1: relative
- r['rdesc'] = "[%s]" % RESOURCE_DESCRIPTIONS.get(r['id'], "?")
- logger.debug(INDENT_OUTPUT(1, "Resource: %s" % r))
- logger.debug(INDENT_OUTPUT(1, "0x%(at)06x| type:%(type)s, id:%(id)5d, size:0x%(size)04x %(rdesc)s '%(short)s'" % r))
- self.ressources.append(r)
- return 4 + 2 + self._pad2(1 + r['namelen']) + 4 + self._pad2(r['size'])
- logger.debug("")
- logger.debug("# Ressources #")
- self.ressources = []
- (n,) = self._readf(">L") # (n,) is a 1-tuple.
- while n > 0:
- n -= parse_irb()
- if n != 0:
- logger.debug("Image resources overran expected size by %d bytes" % (-n))
- def parse_image(self, li, is_layer=True):
- def parse_channel(li, idx, count, rows, cols, depth):
- """params:
- li -- layer info struct
- idx -- channel number
- count -- number of channels to process ???
- rows, cols -- dimensions
- depth -- bits
- """
- chlen = li['chlengths'][idx]
- if chlen is not None and chlen < 2:
- raise ValueError("Not enough channel data: %s" % chlen)
- if li['chids'][idx] == -2:
- rows, cols = li['mask']['rows'], li['mask']['cols']
- rb = (cols * depth + 7) / 8 # round to next byte
- # channel header
- chpos = self.fd.tell()
- (comp,) = self._readf(">H")
- if chlen:
- chlen -= 2
- pos = self.fd.tell()
- # If empty layer
- if cols * rows == 0:
- logger.debug(INDENT_OUTPUT(1, "Empty channel, skiping"))
- return
- if COMPRESSIONS.get(comp) == 'RLE':
- logger.debug(INDENT_OUTPUT(1, "Handling RLE compressed data"))
- rlecounts = 2 * count * rows
- if chlen and chlen < rlecounts:
- raise ValueError("Channel too short for RLE row counts (need %d bytes, have %d bytes)" % (rlecounts,chlen))
- pos += rlecounts # image data starts after RLE counts
- rlecounts_data = self._readf(">%dH" % (count * rows))
- for ch in range(count):
- rlelen_for_channel = sum(rlecounts_data[ch * rows:(ch + 1) * rows])
- data = self.fd.read(rlelen_for_channel)
- channel_name = CHANNEL_SUFFIXES[li['chids'][idx]]
- if li['channels'] == 2 and channel_name == 'B': channel_name = 'L'
- p = Image.fromstring("L", (cols, rows), data, "packbits", "L" )
- if is_layer:
- if channel_name in PIL_BANDS:
- self.images[li['idx']][PIL_BANDS[channel_name]] = p
- else:
- self.merged_image.append(p)
- elif COMPRESSIONS.get(comp) == 'Raw':
- logger.debug(INDENT_OUTPUT(1, "Handling Raw compressed data"))
- for ch in range(count):
- data = self.fd.read(cols * rows)
- channel_name = CHANNEL_SUFFIXES[li['chids'][idx]]
- if li['channels'] == 2 and channel_name == 'B': channel_name = 'L'
- p = Image.fromstring("L", (cols, rows), data, "raw", "L")
- if is_layer:
- if channel_name in PIL_BANDS:
- self.images[li['idx']][PIL_BANDS[channel_name]] = p
- else:
- self.merged_image.append(p)
- else:
- # TODO: maybe just skip channel...:
- # f.seek(chlen, SEEK_CUR)
- # return
- raise ValueError("Unsupported compression type: %s" % COMPRESSIONS.get(comp, comp))
- if (chlen is not None) and (self.fd.tell() != chpos + 2 + chlen):
- logger.debug("currentpos:%d should be:%d!" % (f.tell(), chpos + 2 + chlen))
- self.fd.seek(chpos + 2 + chlen, 0) # 0: SEEK_SET
- return
- if not self.header:
- self.parse_header()
- if not self.ressources:
- self._skip_block("image resources", new_line=True)
- self.ressources = 'not parsed'
- logger.debug("")
- logger.debug("# Image: %s/%d #" % (li['name'], li['channels']))
- # channels
- if is_layer:
- for ch in range(li['channels']):
- parse_channel(li, ch, 1, li['rows'], li['cols'], self.header['depth'])
- else:
- parse_channel(li, 0, li['channels'], li['rows'], li['cols'], self.header['depth'])
- return
- def _read_descriptor(self):
- # Descriptor
- def _unicode_string():
- len = self._readf(">L")[0]
- result = u''
- for count in range(len):
- val = self._readf(">H")[0]
- if val:
- result += unichr(val)
- return result
- def _string_or_key():
- len = self._readf(">L")[0]
- if not len:
- len = 4
- return self._readf(">%ds" % len)[0]
- def _desc_TEXT():
- return _unicode_string()
- def _desc_enum():
- return { 'typeID' : _string_or_key(),
- 'enum' : _string_or_key(),
- }
- def _desc_long():
- return self._readf(">l")[0]
- def _desc_bool():
- return self._readf(">?")[0]
- def _desc_doub():
- return self._readf(">d")[0]
- def _desc_tdta():
- # Apparently it is pdf data?
- # http://telegraphics.com.au/svn/psdparse
- # descriptor.c pdf.c
- len = self._readf(">L")[0]
- pdf_data = self.fd.read(len)
- return pdf_data
- _desc_item_factory = {
- 'TEXT' : _desc_TEXT,
- 'enum' : _desc_enum,
- 'long' : _desc_long,
- 'bool' : _desc_bool,
- 'doub' : _desc_doub,
- 'tdta' : _desc_tdta,
- }
- class_id_name = _unicode_string()
- class_id = _string_or_key()
- logger.debug(INDENT_OUTPUT(4, u"name='%s' clsid='%s'" % (class_id_name, class_id)))
- item_count = self._readf(">L")[0]
- #logger.debug(INDENT_OUTPUT(4, "item_count=%d" % (item_count)))
- items = {}
- for item_index in range(item_count):
- item_key = _string_or_key()
- item_type = self._readf(">4s")[0]
- if not item_type in _desc_item_factory:
- logger.debug(INDENT_OUTPUT(4, "unknown descriptor item '%s', skipping ahead." % item_type))
- break
- items[item_key] = _desc_item_factory[item_type]()
- #logger.debug(INDENT_OUTPUT(4, "item['%s']='%r'" % (item_key,items[item_key])))
- #print items[item_key]
- return items
- def parse_layers_and_masks(self):
- if not self.header:
- self.parse_header()
- if not self.ressources:
- self._skip_block('image resources', new_line=True)
- self.ressources = 'not parsed'
- logger.debug("")
- logger.debug("# Layers & Masks #")
- self.layers = []
- self.images = []
- self.header['mergedalpha'] = False
- (misclen,) = self._readf(">L")
- if misclen:
- miscstart = self.fd.tell()
- # process layer info section
- (layerlen,) = self._readf(">L")
- if layerlen:
- # layers structure
- (self.num_layers,) = self._readf(">h")
- if self.num_layers < 0:
- self.num_layers = -self.num_layers
- logger.debug(INDENT_OUTPUT(1, "First alpha is transparency for merged image"))
- self.header['mergedalpha'] = True
- logger.debug(INDENT_OUTPUT(1, "Layer info for %d layers:" % self.num_layers))
- if self.num_layers * (18 + 6 * self.header['channels']) > layerlen:
- raise ValueError("Unlikely number of %s layers for %s channels with %s layerlen. Giving up." % (self.num_layers, self.header['channels'], layerlen))
- linfo = [] # collect header infos here
- for i in range(self.num_layers):
- l = {}
- l['idx'] = i
- #
- # Layer Info
- #
- (l['top'], l['left'], l['bottom'], l['right'], l['channels']) = self._readf(">llllH")
- (l['rows'], l['cols']) = (l['bottom'] - l['top'], l['right'] - l['left'])
- logger.debug(INDENT_OUTPUT(1, "layer %(idx)d: (%(left)4d,%(top)4d,%(right)4d,%(bottom)4d), %(channels)d channels (%(cols)4d cols x %(rows)4d rows)" % l))
- # Sanity check
- if l['bottom'] < l['top'] or l['right'] < l['left'] or l['channels'] > 64:
- logger.debug(INDENT_OUTPUT(2, "Something's not right about that, trying to skip layer."))
- self.fd.seek(6 * l['channels'] + 12, 1) # 1: SEEK_CUR
- self._skip_block("layer info: extra data", 2)
- continue # next layer
- # Read channel infos
- l['chlengths'] = []
- l['chids'] = []
- # - 'hackish': addressing with -1 and -2 will wrap around to the two extra channels
- l['chindex'] = [ -1 ] * (l['channels'] + 2)
- for j in range(l['channels']):
- chid, chlen = self._readf(">hL")
- l['chids'].append(chid)
- l['chlengths'].append(chlen)
- logger.debug(INDENT_OUTPUT(3, "Channel %2d: id=%2d, %5d bytes" % (j, chid, chlen)))
- if -2 <= chid < l['channels']:
- # pythons negative list-indexs: [ 0, 1, 2, 3, ... -2, -1]
- l['chindex'][chid] = j
- else:
- logger.debug(INDENT_OUTPUT(3, "Unexpected channel id %d" % chid))
- l['chidstr'] = CHANNEL_SUFFIXES.get(chid, "?")
- # put channel info into connection
- linfo.append(l)
- #
- # Blend mode
- #
- bm = {}
- (bm['sig'], bm['key'], bm['opacity'], bm['clipping'], bm['flags'], bm['filler'],
- ) = self._readf(">4s4sBBBB")
- bm['opacp'] = (bm['opacity'] * 100 + 127) / 255
- bm['clipname'] = bm['clipping'] and "non-base" or "base"
- bm['blending'] = BLENDINGS.get(bm['key'])
- l['blend_mode'] = bm
- logger.debug(INDENT_OUTPUT(3, "Blending mode: sig=%(sig)s key=%(key)s opacity=%(opacity)d(%(opacp)d%%) clipping=%(clipping)d(%(clipname)s) flags=%(flags)x" % bm))
- # remember position for skipping unrecognized data
- (extralen,) = self._readf(">L")
- extrastart = self.fd.tell()
- #
- # Layer mask data
- #
- m = {}
- (m['size'],) = self._readf(">L")
- if m['size']:
- (m['top'], m['left'], m['bottom'], m['right'], m['default_color'], m['flags'],
- ) = self._readf(">llllBB")
- # skip remainder
- self.fd.seek(m['size'] - 18, 1) # 1: SEEK_CUR
- m['rows'], m['cols'] = m['bottom'] - m['top'], m['right'] - m['left']
- l['mask'] = m
- self._skip_block("layer blending ranges", 3)
- #
- # Layer name
- #
- name_start = self.fd.tell()
- (l['namelen'],) = self._readf(">B")
- addl_layer_data_start = name_start + self._pad4(l['namelen'] + 1)
- # - "-1": one byte traling 0byte. "-1": one byte garble.
- # (l['name'],) = readf(f, ">%ds" % (self._pad4(1+l['namelen'])-2))
- (l['name'],) = self._readf(">%ds" % (l['namelen']))
- logger.debug(INDENT_OUTPUT(3, "Name: '%s'" % l['name']))
- self.fd.seek(addl_layer_data_start, 0)
- #
- # Read add'l Layer Information
- #
- while self.fd.tell() < extrastart + extralen:
- (signature, key, size, ) = self._readf(">4s4sL") # (n,) is a 1-tuple.
- logger.debug(INDENT_OUTPUT(3, "Addl info: sig='%s' key='%s' size='%d'" %
- (signature, key, size)))
- next_addl_offset = self.fd.tell() + self._pad2(size)
- if key == 'luni':
- namelen = self._readf(">L")[0]
- l['name'] = u''
- for count in range(0, namelen):
- l['name'] += unichr(self._readf(">H")[0])
- logger.debug(INDENT_OUTPUT(4, u"Unicode Name: '%s'" % l['name']))
- elif key == 'TySh':
- version = self._readf(">H")[0]
- (xx, xy, yx, yy, tx, ty,) = self._readf(">dddddd") #transform
- text_version = self._readf(">H")[0]
- text_desc_version = self._readf(">L")[0]
- text_desc = self._read_descriptor()
- warp_version = self._readf(">H")[0]
- warp_desc_version = self._readf(">L")[0]
- warp_desc = self._read_descriptor()
- (left,top,right,bottom,) = self._readf(">llll")
- logger.debug(INDENT_OUTPUT(4, "ver=%d tver=%d dver=%d"
- % (version, text_version, text_desc_version)))
- logger.debug(INDENT_OUTPUT(4, "%f %f %f %f %f %f" % (xx, xy, yx, yy, tx, ty,)))
- logger.debug(INDENT_OUTPUT(4, "l=%f t=%f r=%f b=%f"
- % (left, top, right, bottom)))
- l['text_layer'] = {}
- l['text_layer']['text_desc'] = text_desc
- l['text_layer']['text_transform'] = (xx, xy, yx, yy, tx, ty,)
- l['text_layer']['left'] = left
- l['text_layer']['top'] = top
- l['text_layer']['right'] = right
- l['text_layer']['bottom'] = bottom
- self.fd.seek(next_addl_offset, 0)
- # Skip over any extra data
- self.fd.seek(extrastart + extralen, 0) # 0: SEEK_SET
- self.layers.append(l)
- for i in range(self.num_layers):
- # Empty layer
- if linfo[i]['rows'] * linfo[i]['cols'] == 0:
- self.images.append(None)
- self.parse_image(linfo[i], is_layer=True)
- continue
- self.images.append([0, 0, 0, 0])
- self.parse_image(linfo[i], is_layer=True)
- if linfo[i]['channels'] == 2:
- l = self.images[i][0]
- a = self.images[i][3]
- self.images[i] = Image.merge('LA', [l, a])
- else:
- # is there an alpha channel?
- if type(self.images[i][3]) is int:
- self.images[i] = Image.merge('RGB', self.images[i][0:3])
- else:
- self.images[i] = Image.merge('RGBA', self.images[i])
- else:
- logger.debug(INDENT_OUTPUT(1, "Layer info section is empty"))
- skip = miscstart + misclen - self.fd.tell()
- if skip:
- logger.debug("")
- logger.debug("Skipped %d bytes at end of misc data?" % skip)
- self.fd.seek(skip, 1) # 1: SEEK_CUR
- else:
- logger.debug(INDENT_OUTPUT(1, "Misc info section is empty"))
- def parse_image_data(self):
- if not self.header:
- self.parse_header()
- if not self.ressources:
- self._skip_block("image resources", new_line=True)
- self.ressources = 'not parsed'
- if not self.layers:
- self._skip_block("image layers", new_line=True)
- self.layers = 'not parsed'
- self.merged_image = []
- li = {}
- li['chids'] = range(self.header['channels'])
- li['chlengths'] = [ None ] * self.header['channels'] # dummy data
- (li['name'], li['channels'], li['rows'], li['cols']) = ('merged', self.header['channels'], self.header['rows'], self.header['cols'])
- li['layernum'] = -1
- self.parse_image(li, is_layer=False)
- if li['channels'] == 1:
- self.merged_image = self.merged_image[0]
- elif li['channels'] == 3:
- self.merged_image = Image.merge('RGB', self.merged_image)
- elif li['channels'] >= 4 and self.header['mode'] == 3:
- self.merged_image = Image.merge('RGBA', self.merged_image[:4])
- else:
- raise ValueError('Unsupported mode or number of channels')
|