diff options
Diffstat (limited to 'build/pypng/iccp.py')
-rw-r--r-- | build/pypng/iccp.py | 537 |
1 files changed, 537 insertions, 0 deletions
diff --git a/build/pypng/iccp.py b/build/pypng/iccp.py new file mode 100644 index 0000000..190db73 --- /dev/null +++ b/build/pypng/iccp.py @@ -0,0 +1,537 @@ +#!/usr/bin/env python +# $URL: http://pypng.googlecode.com/svn/trunk/code/iccp.py $ +# $Rev: 182 $ + +# iccp +# +# International Color Consortium Profile +# +# Tools for manipulating ICC profiles. +# +# An ICC profile can be extracted from a PNG image (iCCP chunk). +# +# +# Non-standard ICCP tags. +# +# Apple use some (widespread but) non-standard tags. These can be +# displayed in Apple's ColorSync Utility. +# - 'vcgt' (Video Card Gamma Tag). Table to load into video +# card LUT to apply gamma. +# - 'ndin' Apple display native information. +# - 'dscm' Apple multi-localized description strings. +# - 'mmod' Apple display make and model information. +# + +# References +# +# [ICC 2001] ICC Specification ICC.1:2001-04 (Profile version 2.4.0) +# [ICC 2004] ICC Specification ICC.1:2004-10 (Profile version 4.2.0.0) + +import struct + +import png + +class FormatError(Exception): + pass + +class Profile: + """An International Color Consortium Profile (ICC Profile).""" + + def __init__(self): + self.rawtagtable = None + self.rawtagdict = {} + self.d = dict() + + def fromFile(self, inp, name='<unknown>'): + + # See [ICC 2004] + profile = inp.read(128) + if len(profile) < 128: + raise FormatError("ICC Profile is too short.") + size, = struct.unpack('>L', profile[:4]) + profile += inp.read(d['size'] - len(profile)) + return self.fromString(profile, name) + + def fromString(self, profile, name='<unknown>'): + self.d = dict() + d = self.d + if len(profile) < 128: + raise FormatError("ICC Profile is too short.") + d.update( + zip(['size', 'preferredCMM', 'version', + 'profileclass', 'colourspace', 'pcs'], + struct.unpack('>L4sL4s4s4s', profile[:24]))) + if len(profile) < d['size']: + warnings.warn( + 'Profile size declared to be %d, but only got %d bytes' % + (d['size'], len(profile))) + d['version'] = '%08x' % d['version'] + d['created'] = readICCdatetime(profile[24:36]) + d.update( + zip(['acsp', 'platform', 'flag', 'manufacturer', 'model'], + struct.unpack('>4s4s3L', profile[36:56]))) + if d['acsp'] != 'acsp': + warnings.warn('acsp field not present (not an ICC Profile?).') + d['deviceattributes'] = profile[56:64] + d['intent'], = struct.unpack('>L', profile[64:68]) + d['pcsilluminant'] = readICCXYZNumber(profile[68:80]) + d['creator'] = profile[80:84] + d['id'] = profile[84:100] + ntags, = struct.unpack('>L', profile[128:132]) + d['ntags'] = ntags + fmt = '4s2L' * ntags + # tag table + tt = struct.unpack('>' + fmt, profile[132:132+12*ntags]) + tt = group(tt, 3) + + # Could (should) detect 2 or more tags having the same sig. But + # we don't. Two or more tags with the same sig is illegal per + # the ICC spec. + + # Convert (sig,offset,size) triples into (sig,value) pairs. + rawtag = map(lambda x: (x[0], profile[x[1]:x[1]+x[2]]), tt) + self.rawtagtable = rawtag + self.rawtagdict = dict(rawtag) + tag = dict() + # Interpret the tags whose types we know about + for sig, v in rawtag: + if sig in tag: + warnings.warn("Duplicate tag %r found. Ignoring." % sig) + continue + v = ICCdecode(v) + if v is not None: + tag[sig] = v + self.tag = tag + return self + + def greyInput(self): + """Adjust ``self.d`` dictionary for greyscale input device. + ``profileclass`` is 'scnr', ``colourspace`` is 'GRAY', ``pcs`` + is 'XYZ '. + """ + + self.d.update(dict(profileclass='scnr', + colourspace='GRAY', pcs='XYZ ')) + return self + + def maybeAddDefaults(self): + if self.rawtagdict: + return + self._addTags( + cprt='Copyright unknown.', + desc='created by $URL: http://pypng.googlecode.com/svn/trunk/code/iccp.py $ $Rev: 182 $', + wtpt=D50(), + ) + + def addTags(self, **k): + self.maybeAddDefaults() + self._addTags(**k) + + def _addTags(self, **k): + """Helper for :meth:`addTags`.""" + + for tag, thing in k.items(): + if not isinstance(thing, (tuple, list)): + thing = (thing,) + typetag = defaulttagtype[tag] + self.rawtagdict[tag] = encode(typetag, *thing) + return self + + def write(self, out): + """Write ICC Profile to the file.""" + + if not self.rawtagtable: + self.rawtagtable = self.rawtagdict.items() + tags = tagblock(self.rawtagtable) + self.writeHeader(out, 128 + len(tags)) + out.write(tags) + out.flush() + + return self + + def writeHeader(self, out, size=999): + """Add default values to the instance's `d` dictionary, then + write a header out onto the file stream. The size of the + profile must be specified using the `size` argument. + """ + + def defaultkey(d, key, value): + """Add ``[key]==value`` to the dictionary `d`, but only if + it does not have that key already. + """ + + if key in d: + return + d[key] = value + + z = '\x00' * 4 + defaults = dict(preferredCMM=z, + version='02000000', + profileclass=z, + colourspace=z, + pcs='XYZ ', + created=writeICCdatetime(), + acsp='acsp', + platform=z, + flag=0, + manufacturer=z, + model=0, + deviceattributes=0, + intent=0, + pcsilluminant=encodefuns()['XYZ'](*D50()), + creator=z, + ) + for k,v in defaults.items(): + defaultkey(self.d, k, v) + + hl = map(self.d.__getitem__, + ['preferredCMM', 'version', 'profileclass', 'colourspace', + 'pcs', 'created', 'acsp', 'platform', 'flag', + 'manufacturer', 'model', 'deviceattributes', 'intent', + 'pcsilluminant', 'creator']) + # Convert to struct.pack input + hl[1] = int(hl[1], 16) + + out.write(struct.pack('>L4sL4s4s4s12s4s4sL4sLQL12s4s', size, *hl)) + out.write('\x00' * 44) + return self + +def encodefuns(): + """Returns a dictionary mapping ICC type signature sig to encoding + function. Each function returns a string comprising the content of + the encoded value. To form the full value, the type sig and the 4 + zero bytes should be prefixed (8 bytes). + """ + + def desc(ascii): + """Return textDescription type [ICC 2001] 6.5.17. The ASCII part is + filled in with the string `ascii`, the Unicode and ScriptCode parts + are empty.""" + + ascii += '\x00' + l = len(ascii) + + return struct.pack('>L%ds2LHB67s' % l, + l, ascii, 0, 0, 0, 0, '') + + def text(ascii): + """Return textType [ICC 2001] 6.5.18.""" + + return ascii + '\x00' + + def curv(f=None, n=256): + """Return a curveType, [ICC 2001] 6.5.3. If no arguments are + supplied then a TRC for a linear response is generated (no entries). + If an argument is supplied and it is a number (for *f* to be a + number it means that ``float(f)==f``) then a TRC for that + gamma value is generated. + Otherwise `f` is assumed to be a function that maps [0.0, 1.0] to + [0.0, 1.0]; an `n` element table is generated for it. + """ + + if f is None: + return struct.pack('>L', 0) + try: + if float(f) == f: + return struct.pack('>LH', 1, int(round(f*2**8))) + except (TypeError, ValueError): + pass + assert n >= 2 + table = [] + M = float(n-1) + for i in range(n): + x = i/M + table.append(int(round(f(x) * 65535))) + return struct.pack('>L%dH' % n, n, *table) + + def XYZ(*l): + return struct.pack('>3l', *map(fs15f16, l)) + + return locals() + +# Tag type defaults. +# Most tags can only have one or a few tag types. +# When encoding, we associate a default tag type with each tag so that +# the encoding is implicit. +defaulttagtype=dict( + A2B0='mft1', + A2B1='mft1', + A2B2='mft1', + bXYZ='XYZ', + bTRC='curv', + B2A0='mft1', + B2A1='mft1', + B2A2='mft1', + calt='dtim', + targ='text', + chad='sf32', + chrm='chrm', + cprt='desc', + crdi='crdi', + dmnd='desc', + dmdd='desc', + devs='', + gamt='mft1', + kTRC='curv', + gXYZ='XYZ', + gTRC='curv', + lumi='XYZ', + meas='', + bkpt='XYZ', + wtpt='XYZ', + ncol='', + ncl2='', + resp='', + pre0='mft1', + pre1='mft1', + pre2='mft1', + desc='desc', + pseq='', + psd0='data', + psd1='data', + psd2='data', + psd3='data', + ps2s='data', + ps2i='data', + rXYZ='XYZ', + rTRC='curv', + scrd='desc', + scrn='', + tech='sig', + bfd='', + vued='desc', + view='view', +) + +def encode(tsig, *l): + """Encode a Python value as an ICC type. `tsig` is the type + signature to (the first 4 bytes of the encoded value, see [ICC 2004] + section 10. + """ + + fun = encodefuns() + if tsig not in fun: + raise "No encoder for type %r." % tsig + v = fun[tsig](*l) + # Padd tsig out with spaces. + tsig = (tsig + ' ')[:4] + return tsig + '\x00'*4 + v + +def tagblock(tag): + """`tag` should be a list of (*signature*, *element*) pairs, where + *signature* (the key) is a length 4 string, and *element* is the + content of the tag element (another string). + + The entire tag block (consisting of first a table and then the + element data) is constructed and returned as a string. + """ + + n = len(tag) + tablelen = 12*n + + # Build the tag table in two parts. A list of 12-byte tags, and a + # string of element data. Offset is the offset from the start of + # the profile to the start of the element data (so the offset for + # the next element is this offset plus the length of the element + # string so far). + offset = 128 + tablelen + 4 + # The table. As a string. + table = '' + # The element data + element = '' + for k,v in tag: + table += struct.pack('>4s2L', k, offset + len(element), len(v)) + element += v + return struct.pack('>L', n) + table + element + +def iccp(out, inp): + profile = Profile().fromString(*profileFromPNG(inp)) + print >>out, profile.d + print >>out, map(lambda x: x[0], profile.rawtagtable) + print >>out, profile.tag + +def profileFromPNG(inp): + """Extract profile from PNG file. Return (*profile*, *name*) + pair.""" + r = png.Reader(file=inp) + _,chunk = r.chunk('iCCP') + i = chunk.index('\x00') + name = chunk[:i] + compression = chunk[i+1] + assert compression == chr(0) + profile = chunk[i+2:].decode('zlib') + return profile, name + +def iccpout(out, inp): + """Extract ICC Profile from PNG file `inp` and write it to + the file `out`.""" + + out.write(profileFromPNG(inp)[0]) + +def fs15f16(x): + """Convert float to ICC s15Fixed16Number (as a Python ``int``).""" + + return int(round(x * 2**16)) + +def D50(): + """Return D50 illuminant as an (X,Y,Z) triple.""" + + # See [ICC 2001] A.1 + return (0.9642, 1.0000, 0.8249) + + +def writeICCdatetime(t=None): + """`t` should be a gmtime tuple (as returned from + ``time.gmtime()``). If not supplied, the current time will be used. + Return an ICC dateTimeNumber in a 12 byte string. + """ + + import time + if t is None: + t = time.gmtime() + return struct.pack('>6H', *t[:6]) + +def readICCdatetime(s): + """Convert from 12 byte ICC representation of dateTimeNumber to + ISO8601 string. See [ICC 2004] 5.1.1""" + + return '%04d-%02d-%02dT%02d:%02d:%02dZ' % struct.unpack('>6H', s) + +def readICCXYZNumber(s): + """Convert from 12 byte ICC representation of XYZNumber to (x,y,z) + triple of floats. See [ICC 2004] 5.1.11""" + + return s15f16l(s) + +def s15f16l(s): + """Convert sequence of ICC s15Fixed16 to list of float.""" + # Note: As long as float has at least 32 bits of mantissa, all + # values are preserved. + n = len(s)//4 + t = struct.unpack('>%dl' % n, s) + return map((2**-16).__mul__, t) + +# Several types and their byte encodings are defined by [ICC 2004] +# section 10. When encoded, a value begins with a 4 byte type +# signature. We use the same 4 byte type signature in the names of the +# Python functions that decode the type into a Pythonic representation. + +def ICCdecode(s): + """Take an ICC encoded tag, and dispatch on its type signature + (first 4 bytes) to decode it into a Python value. Pair (*sig*, + *value*) is returned, where *sig* is a 4 byte string, and *value* is + some Python value determined by the content and type. + """ + + sig = s[0:4].strip() + f=dict(text=RDtext, + XYZ=RDXYZ, + curv=RDcurv, + vcgt=RDvcgt, + sf32=RDsf32, + ) + if sig not in f: + return None + return (sig, f[sig](s)) + +def RDXYZ(s): + """Convert ICC XYZType to rank 1 array of trimulus values.""" + + # See [ICC 2001] 6.5.26 + assert s[0:4] == 'XYZ ' + return readICCXYZNumber(s[8:]) + +def RDsf32(s): + """Convert ICC s15Fixed16ArrayType to list of float.""" + # See [ICC 2004] 10.18 + assert s[0:4] == 'sf32' + return s15f16l(s[8:]) + +def RDmluc(s): + """Convert ICC multiLocalizedUnicodeType. This types encodes + several strings together with a language/country code for each + string. A list of (*lc*, *string*) pairs is returned where *lc* is + the 4 byte language/country code, and *string* is the string + corresponding to that code. It seems unlikely that the same + language/country code will appear more than once with different + strings, but the ICC standard does not prohibit it.""" + # See [ICC 2004] 10.13 + assert s[0:4] == 'mluc' + n,sz = struct.unpack('>2L', s[8:16]) + assert sz == 12 + record = [] + for i in range(n): + lc,l,o = struct.unpack('4s2L', s[16+12*n:28+12*n]) + record.append(lc, s[o:o+l]) + # How are strings encoded? + return record + +def RDtext(s): + """Convert ICC textType to Python string.""" + # Note: type not specified or used in [ICC 2004], only in older + # [ICC 2001]. + # See [ICC 2001] 6.5.18 + assert s[0:4] == 'text' + return s[8:-1] + +def RDcurv(s): + """Convert ICC curveType.""" + # See [ICC 2001] 6.5.3 + assert s[0:4] == 'curv' + count, = struct.unpack('>L', s[8:12]) + if count == 0: + return dict(gamma=1) + table = struct.unpack('>%dH' % count, s[12:]) + if count == 1: + return dict(gamma=table[0]*2**-8) + return table + +def RDvcgt(s): + """Convert Apple CMVideoCardGammaType.""" + # See + # http://developer.apple.com/documentation/GraphicsImaging/Reference/ColorSync_Manager/Reference/reference.html#//apple_ref/c/tdef/CMVideoCardGammaType + assert s[0:4] == 'vcgt' + tagtype, = struct.unpack('>L', s[8:12]) + if tagtype != 0: + return s[8:] + if tagtype == 0: + # Table. + channels,count,size = struct.unpack('>3H', s[12:18]) + if size == 1: + fmt = 'B' + elif size == 2: + fmt = 'H' + else: + return s[8:] + l = len(s[18:])//size + t = struct.unpack('>%d%s' % (l, fmt), s[18:]) + t = group(t, count) + return size, t + return s[8:] + + +def group(s, n): + # See + # http://www.python.org/doc/2.6/library/functions.html#zip + return zip(*[iter(s)]*n) + + +def main(argv=None): + import sys + from getopt import getopt + if argv is None: + argv = sys.argv + argv = argv[1:] + opt,arg = getopt(argv, 'o:') + if len(arg) > 0: + inp = open(arg[0], 'rb') + else: + inp = sys.stdin + for o,v in opt: + if o == '-o': + f = open(v, 'wb') + return iccpout(f, inp) + return iccp(sys.stdout, inp) + +if __name__ == '__main__': + main() |