iccp.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. #!/usr/bin/env python
  2. # $URL: http://pypng.googlecode.com/svn/trunk/code/iccp.py $
  3. # $Rev: 182 $
  4. # iccp
  5. #
  6. # International Color Consortium Profile
  7. #
  8. # Tools for manipulating ICC profiles.
  9. #
  10. # An ICC profile can be extracted from a PNG image (iCCP chunk).
  11. #
  12. #
  13. # Non-standard ICCP tags.
  14. #
  15. # Apple use some (widespread but) non-standard tags. These can be
  16. # displayed in Apple's ColorSync Utility.
  17. # - 'vcgt' (Video Card Gamma Tag). Table to load into video
  18. # card LUT to apply gamma.
  19. # - 'ndin' Apple display native information.
  20. # - 'dscm' Apple multi-localized description strings.
  21. # - 'mmod' Apple display make and model information.
  22. #
  23. # References
  24. #
  25. # [ICC 2001] ICC Specification ICC.1:2001-04 (Profile version 2.4.0)
  26. # [ICC 2004] ICC Specification ICC.1:2004-10 (Profile version 4.2.0.0)
  27. import struct
  28. import png
  29. class FormatError(Exception):
  30. pass
  31. class Profile:
  32. """An International Color Consortium Profile (ICC Profile)."""
  33. def __init__(self):
  34. self.rawtagtable = None
  35. self.rawtagdict = {}
  36. self.d = dict()
  37. def fromFile(self, inp, name='<unknown>'):
  38. # See [ICC 2004]
  39. profile = inp.read(128)
  40. if len(profile) < 128:
  41. raise FormatError("ICC Profile is too short.")
  42. size, = struct.unpack('>L', profile[:4])
  43. profile += inp.read(d['size'] - len(profile))
  44. return self.fromString(profile, name)
  45. def fromString(self, profile, name='<unknown>'):
  46. self.d = dict()
  47. d = self.d
  48. if len(profile) < 128:
  49. raise FormatError("ICC Profile is too short.")
  50. d.update(
  51. zip(['size', 'preferredCMM', 'version',
  52. 'profileclass', 'colourspace', 'pcs'],
  53. struct.unpack('>L4sL4s4s4s', profile[:24])))
  54. if len(profile) < d['size']:
  55. warnings.warn(
  56. 'Profile size declared to be %d, but only got %d bytes' %
  57. (d['size'], len(profile)))
  58. d['version'] = '%08x' % d['version']
  59. d['created'] = readICCdatetime(profile[24:36])
  60. d.update(
  61. zip(['acsp', 'platform', 'flag', 'manufacturer', 'model'],
  62. struct.unpack('>4s4s3L', profile[36:56])))
  63. if d['acsp'] != 'acsp':
  64. warnings.warn('acsp field not present (not an ICC Profile?).')
  65. d['deviceattributes'] = profile[56:64]
  66. d['intent'], = struct.unpack('>L', profile[64:68])
  67. d['pcsilluminant'] = readICCXYZNumber(profile[68:80])
  68. d['creator'] = profile[80:84]
  69. d['id'] = profile[84:100]
  70. ntags, = struct.unpack('>L', profile[128:132])
  71. d['ntags'] = ntags
  72. fmt = '4s2L' * ntags
  73. # tag table
  74. tt = struct.unpack('>' + fmt, profile[132:132+12*ntags])
  75. tt = group(tt, 3)
  76. # Could (should) detect 2 or more tags having the same sig. But
  77. # we don't. Two or more tags with the same sig is illegal per
  78. # the ICC spec.
  79. # Convert (sig,offset,size) triples into (sig,value) pairs.
  80. rawtag = map(lambda x: (x[0], profile[x[1]:x[1]+x[2]]), tt)
  81. self.rawtagtable = rawtag
  82. self.rawtagdict = dict(rawtag)
  83. tag = dict()
  84. # Interpret the tags whose types we know about
  85. for sig, v in rawtag:
  86. if sig in tag:
  87. warnings.warn("Duplicate tag %r found. Ignoring." % sig)
  88. continue
  89. v = ICCdecode(v)
  90. if v is not None:
  91. tag[sig] = v
  92. self.tag = tag
  93. return self
  94. def greyInput(self):
  95. """Adjust ``self.d`` dictionary for greyscale input device.
  96. ``profileclass`` is 'scnr', ``colourspace`` is 'GRAY', ``pcs``
  97. is 'XYZ '.
  98. """
  99. self.d.update(dict(profileclass='scnr',
  100. colourspace='GRAY', pcs='XYZ '))
  101. return self
  102. def maybeAddDefaults(self):
  103. if self.rawtagdict:
  104. return
  105. self._addTags(
  106. cprt='Copyright unknown.',
  107. desc='created by $URL: http://pypng.googlecode.com/svn/trunk/code/iccp.py $ $Rev: 182 $',
  108. wtpt=D50(),
  109. )
  110. def addTags(self, **k):
  111. self.maybeAddDefaults()
  112. self._addTags(**k)
  113. def _addTags(self, **k):
  114. """Helper for :meth:`addTags`."""
  115. for tag, thing in k.items():
  116. if not isinstance(thing, (tuple, list)):
  117. thing = (thing,)
  118. typetag = defaulttagtype[tag]
  119. self.rawtagdict[tag] = encode(typetag, *thing)
  120. return self
  121. def write(self, out):
  122. """Write ICC Profile to the file."""
  123. if not self.rawtagtable:
  124. self.rawtagtable = self.rawtagdict.items()
  125. tags = tagblock(self.rawtagtable)
  126. self.writeHeader(out, 128 + len(tags))
  127. out.write(tags)
  128. out.flush()
  129. return self
  130. def writeHeader(self, out, size=999):
  131. """Add default values to the instance's `d` dictionary, then
  132. write a header out onto the file stream. The size of the
  133. profile must be specified using the `size` argument.
  134. """
  135. def defaultkey(d, key, value):
  136. """Add ``[key]==value`` to the dictionary `d`, but only if
  137. it does not have that key already.
  138. """
  139. if key in d:
  140. return
  141. d[key] = value
  142. z = '\x00' * 4
  143. defaults = dict(preferredCMM=z,
  144. version='02000000',
  145. profileclass=z,
  146. colourspace=z,
  147. pcs='XYZ ',
  148. created=writeICCdatetime(),
  149. acsp='acsp',
  150. platform=z,
  151. flag=0,
  152. manufacturer=z,
  153. model=0,
  154. deviceattributes=0,
  155. intent=0,
  156. pcsilluminant=encodefuns()['XYZ'](*D50()),
  157. creator=z,
  158. )
  159. for k,v in defaults.items():
  160. defaultkey(self.d, k, v)
  161. hl = map(self.d.__getitem__,
  162. ['preferredCMM', 'version', 'profileclass', 'colourspace',
  163. 'pcs', 'created', 'acsp', 'platform', 'flag',
  164. 'manufacturer', 'model', 'deviceattributes', 'intent',
  165. 'pcsilluminant', 'creator'])
  166. # Convert to struct.pack input
  167. hl[1] = int(hl[1], 16)
  168. out.write(struct.pack('>L4sL4s4s4s12s4s4sL4sLQL12s4s', size, *hl))
  169. out.write('\x00' * 44)
  170. return self
  171. def encodefuns():
  172. """Returns a dictionary mapping ICC type signature sig to encoding
  173. function. Each function returns a string comprising the content of
  174. the encoded value. To form the full value, the type sig and the 4
  175. zero bytes should be prefixed (8 bytes).
  176. """
  177. def desc(ascii):
  178. """Return textDescription type [ICC 2001] 6.5.17. The ASCII part is
  179. filled in with the string `ascii`, the Unicode and ScriptCode parts
  180. are empty."""
  181. ascii += '\x00'
  182. l = len(ascii)
  183. return struct.pack('>L%ds2LHB67s' % l,
  184. l, ascii, 0, 0, 0, 0, '')
  185. def text(ascii):
  186. """Return textType [ICC 2001] 6.5.18."""
  187. return ascii + '\x00'
  188. def curv(f=None, n=256):
  189. """Return a curveType, [ICC 2001] 6.5.3. If no arguments are
  190. supplied then a TRC for a linear response is generated (no entries).
  191. If an argument is supplied and it is a number (for *f* to be a
  192. number it means that ``float(f)==f``) then a TRC for that
  193. gamma value is generated.
  194. Otherwise `f` is assumed to be a function that maps [0.0, 1.0] to
  195. [0.0, 1.0]; an `n` element table is generated for it.
  196. """
  197. if f is None:
  198. return struct.pack('>L', 0)
  199. try:
  200. if float(f) == f:
  201. return struct.pack('>LH', 1, int(round(f*2**8)))
  202. except (TypeError, ValueError):
  203. pass
  204. assert n >= 2
  205. table = []
  206. M = float(n-1)
  207. for i in range(n):
  208. x = i/M
  209. table.append(int(round(f(x) * 65535)))
  210. return struct.pack('>L%dH' % n, n, *table)
  211. def XYZ(*l):
  212. return struct.pack('>3l', *map(fs15f16, l))
  213. return locals()
  214. # Tag type defaults.
  215. # Most tags can only have one or a few tag types.
  216. # When encoding, we associate a default tag type with each tag so that
  217. # the encoding is implicit.
  218. defaulttagtype=dict(
  219. A2B0='mft1',
  220. A2B1='mft1',
  221. A2B2='mft1',
  222. bXYZ='XYZ',
  223. bTRC='curv',
  224. B2A0='mft1',
  225. B2A1='mft1',
  226. B2A2='mft1',
  227. calt='dtim',
  228. targ='text',
  229. chad='sf32',
  230. chrm='chrm',
  231. cprt='desc',
  232. crdi='crdi',
  233. dmnd='desc',
  234. dmdd='desc',
  235. devs='',
  236. gamt='mft1',
  237. kTRC='curv',
  238. gXYZ='XYZ',
  239. gTRC='curv',
  240. lumi='XYZ',
  241. meas='',
  242. bkpt='XYZ',
  243. wtpt='XYZ',
  244. ncol='',
  245. ncl2='',
  246. resp='',
  247. pre0='mft1',
  248. pre1='mft1',
  249. pre2='mft1',
  250. desc='desc',
  251. pseq='',
  252. psd0='data',
  253. psd1='data',
  254. psd2='data',
  255. psd3='data',
  256. ps2s='data',
  257. ps2i='data',
  258. rXYZ='XYZ',
  259. rTRC='curv',
  260. scrd='desc',
  261. scrn='',
  262. tech='sig',
  263. bfd='',
  264. vued='desc',
  265. view='view',
  266. )
  267. def encode(tsig, *l):
  268. """Encode a Python value as an ICC type. `tsig` is the type
  269. signature to (the first 4 bytes of the encoded value, see [ICC 2004]
  270. section 10.
  271. """
  272. fun = encodefuns()
  273. if tsig not in fun:
  274. raise "No encoder for type %r." % tsig
  275. v = fun[tsig](*l)
  276. # Padd tsig out with spaces.
  277. tsig = (tsig + ' ')[:4]
  278. return tsig + '\x00'*4 + v
  279. def tagblock(tag):
  280. """`tag` should be a list of (*signature*, *element*) pairs, where
  281. *signature* (the key) is a length 4 string, and *element* is the
  282. content of the tag element (another string).
  283. The entire tag block (consisting of first a table and then the
  284. element data) is constructed and returned as a string.
  285. """
  286. n = len(tag)
  287. tablelen = 12*n
  288. # Build the tag table in two parts. A list of 12-byte tags, and a
  289. # string of element data. Offset is the offset from the start of
  290. # the profile to the start of the element data (so the offset for
  291. # the next element is this offset plus the length of the element
  292. # string so far).
  293. offset = 128 + tablelen + 4
  294. # The table. As a string.
  295. table = ''
  296. # The element data
  297. element = ''
  298. for k,v in tag:
  299. table += struct.pack('>4s2L', k, offset + len(element), len(v))
  300. element += v
  301. return struct.pack('>L', n) + table + element
  302. def iccp(out, inp):
  303. profile = Profile().fromString(*profileFromPNG(inp))
  304. print >>out, profile.d
  305. print >>out, map(lambda x: x[0], profile.rawtagtable)
  306. print >>out, profile.tag
  307. def profileFromPNG(inp):
  308. """Extract profile from PNG file. Return (*profile*, *name*)
  309. pair."""
  310. r = png.Reader(file=inp)
  311. _,chunk = r.chunk('iCCP')
  312. i = chunk.index('\x00')
  313. name = chunk[:i]
  314. compression = chunk[i+1]
  315. assert compression == chr(0)
  316. profile = chunk[i+2:].decode('zlib')
  317. return profile, name
  318. def iccpout(out, inp):
  319. """Extract ICC Profile from PNG file `inp` and write it to
  320. the file `out`."""
  321. out.write(profileFromPNG(inp)[0])
  322. def fs15f16(x):
  323. """Convert float to ICC s15Fixed16Number (as a Python ``int``)."""
  324. return int(round(x * 2**16))
  325. def D50():
  326. """Return D50 illuminant as an (X,Y,Z) triple."""
  327. # See [ICC 2001] A.1
  328. return (0.9642, 1.0000, 0.8249)
  329. def writeICCdatetime(t=None):
  330. """`t` should be a gmtime tuple (as returned from
  331. ``time.gmtime()``). If not supplied, the current time will be used.
  332. Return an ICC dateTimeNumber in a 12 byte string.
  333. """
  334. import time
  335. if t is None:
  336. t = time.gmtime()
  337. return struct.pack('>6H', *t[:6])
  338. def readICCdatetime(s):
  339. """Convert from 12 byte ICC representation of dateTimeNumber to
  340. ISO8601 string. See [ICC 2004] 5.1.1"""
  341. return '%04d-%02d-%02dT%02d:%02d:%02dZ' % struct.unpack('>6H', s)
  342. def readICCXYZNumber(s):
  343. """Convert from 12 byte ICC representation of XYZNumber to (x,y,z)
  344. triple of floats. See [ICC 2004] 5.1.11"""
  345. return s15f16l(s)
  346. def s15f16l(s):
  347. """Convert sequence of ICC s15Fixed16 to list of float."""
  348. # Note: As long as float has at least 32 bits of mantissa, all
  349. # values are preserved.
  350. n = len(s)//4
  351. t = struct.unpack('>%dl' % n, s)
  352. return map((2**-16).__mul__, t)
  353. # Several types and their byte encodings are defined by [ICC 2004]
  354. # section 10. When encoded, a value begins with a 4 byte type
  355. # signature. We use the same 4 byte type signature in the names of the
  356. # Python functions that decode the type into a Pythonic representation.
  357. def ICCdecode(s):
  358. """Take an ICC encoded tag, and dispatch on its type signature
  359. (first 4 bytes) to decode it into a Python value. Pair (*sig*,
  360. *value*) is returned, where *sig* is a 4 byte string, and *value* is
  361. some Python value determined by the content and type.
  362. """
  363. sig = s[0:4].strip()
  364. f=dict(text=RDtext,
  365. XYZ=RDXYZ,
  366. curv=RDcurv,
  367. vcgt=RDvcgt,
  368. sf32=RDsf32,
  369. )
  370. if sig not in f:
  371. return None
  372. return (sig, f[sig](s))
  373. def RDXYZ(s):
  374. """Convert ICC XYZType to rank 1 array of trimulus values."""
  375. # See [ICC 2001] 6.5.26
  376. assert s[0:4] == 'XYZ '
  377. return readICCXYZNumber(s[8:])
  378. def RDsf32(s):
  379. """Convert ICC s15Fixed16ArrayType to list of float."""
  380. # See [ICC 2004] 10.18
  381. assert s[0:4] == 'sf32'
  382. return s15f16l(s[8:])
  383. def RDmluc(s):
  384. """Convert ICC multiLocalizedUnicodeType. This types encodes
  385. several strings together with a language/country code for each
  386. string. A list of (*lc*, *string*) pairs is returned where *lc* is
  387. the 4 byte language/country code, and *string* is the string
  388. corresponding to that code. It seems unlikely that the same
  389. language/country code will appear more than once with different
  390. strings, but the ICC standard does not prohibit it."""
  391. # See [ICC 2004] 10.13
  392. assert s[0:4] == 'mluc'
  393. n,sz = struct.unpack('>2L', s[8:16])
  394. assert sz == 12
  395. record = []
  396. for i in range(n):
  397. lc,l,o = struct.unpack('4s2L', s[16+12*n:28+12*n])
  398. record.append(lc, s[o:o+l])
  399. # How are strings encoded?
  400. return record
  401. def RDtext(s):
  402. """Convert ICC textType to Python string."""
  403. # Note: type not specified or used in [ICC 2004], only in older
  404. # [ICC 2001].
  405. # See [ICC 2001] 6.5.18
  406. assert s[0:4] == 'text'
  407. return s[8:-1]
  408. def RDcurv(s):
  409. """Convert ICC curveType."""
  410. # See [ICC 2001] 6.5.3
  411. assert s[0:4] == 'curv'
  412. count, = struct.unpack('>L', s[8:12])
  413. if count == 0:
  414. return dict(gamma=1)
  415. table = struct.unpack('>%dH' % count, s[12:])
  416. if count == 1:
  417. return dict(gamma=table[0]*2**-8)
  418. return table
  419. def RDvcgt(s):
  420. """Convert Apple CMVideoCardGammaType."""
  421. # See
  422. # http://developer.apple.com/documentation/GraphicsImaging/Reference/ColorSync_Manager/Reference/reference.html#//apple_ref/c/tdef/CMVideoCardGammaType
  423. assert s[0:4] == 'vcgt'
  424. tagtype, = struct.unpack('>L', s[8:12])
  425. if tagtype != 0:
  426. return s[8:]
  427. if tagtype == 0:
  428. # Table.
  429. channels,count,size = struct.unpack('>3H', s[12:18])
  430. if size == 1:
  431. fmt = 'B'
  432. elif size == 2:
  433. fmt = 'H'
  434. else:
  435. return s[8:]
  436. l = len(s[18:])//size
  437. t = struct.unpack('>%d%s' % (l, fmt), s[18:])
  438. t = group(t, count)
  439. return size, t
  440. return s[8:]
  441. def group(s, n):
  442. # See
  443. # http://www.python.org/doc/2.6/library/functions.html#zip
  444. return zip(*[iter(s)]*n)
  445. def main(argv=None):
  446. import sys
  447. from getopt import getopt
  448. if argv is None:
  449. argv = sys.argv
  450. argv = argv[1:]
  451. opt,arg = getopt(argv, 'o:')
  452. if len(arg) > 0:
  453. inp = open(arg[0], 'rb')
  454. else:
  455. inp = sys.stdin
  456. for o,v in opt:
  457. if o == '-o':
  458. f = open(v, 'wb')
  459. return iccpout(f, inp)
  460. return iccp(sys.stdout, inp)
  461. if __name__ == '__main__':
  462. main()