pngchunk 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. #!/usr/bin/env python
  2. # $URL: http://pypng.googlecode.com/svn/trunk/code/pngchunk $
  3. # $Rev: 156 $
  4. # pngchunk
  5. # Chunk editing/extraction tool.
  6. import struct
  7. import warnings
  8. # Local module.
  9. import png
  10. """
  11. pngchunk [--gamma g] [--iccprofile file] [--sigbit b] [-c cHNK!] [-c cHNK:foo] [-c cHNK<file]
  12. The ``-c`` option is used to add or remove chunks. A chunk is specified
  13. by its 4 byte chunk type. If this is followed by a ``!`` then that
  14. chunk is removed from the PNG file. If the chunk type is followed by a
  15. ``:`` then the chunk is replaced with the contents of the rest of the
  16. argument (this is probably only useful if the content is mostly ASCII,
  17. otherwise it's a pain to quote the contents, otherwise see...). A ``<``
  18. can be used to take the contents of the chunk from the named file.
  19. """
  20. def chunk(out, inp, l):
  21. """Process the input PNG file to the output, chunk by chunk. Chunks
  22. can be inserted, removed, replaced, or sometimes edited. Generally,
  23. chunks are not inspected, so pixel data (in the ``IDAT`` chunks)
  24. cannot be modified. `l` should be a list of (*chunktype*,
  25. *content*) pairs. *chunktype* is usually the type of the PNG chunk,
  26. specified as a 4-byte Python string, and *content* is the chunk's
  27. content, also as a string; if *content* is ``None`` then *all*
  28. chunks of that type will be removed.
  29. This function *knows* about certain chunk types and will
  30. automatically convert from Python friendly representations to
  31. string-of-bytes.
  32. chunktype
  33. 'gamma' 'gAMA' float
  34. 'sigbit' 'sBIT' int, or tuple of length 1,2 or 3
  35. Note that the length of the strings used to identify *friendly*
  36. chunk types is greater than 4, hence they cannot be confused with
  37. canonical chunk types.
  38. Chunk types, if specified using the 4-byte syntax, need not be
  39. official PNG chunks at all. Non-standard chunks can be created.
  40. """
  41. def canonical(p):
  42. """Take a pair (*chunktype*, *content*), and return canonical
  43. representation (*chunktype*, *content*) where `chunktype` is the
  44. 4-byte PNG chunk type and `content` is a string.
  45. """
  46. t,v = p
  47. if len(t) == 4:
  48. return t,v
  49. if t == 'gamma':
  50. t = 'gAMA'
  51. v = int(round(1e5*v))
  52. v = struct.pack('>I', v)
  53. elif t == 'sigbit':
  54. t = 'sBIT'
  55. try:
  56. v[0]
  57. except TypeError:
  58. v = (v,)
  59. v = struct.pack('%dB' % len(v), *v)
  60. elif t == 'iccprofile':
  61. t = 'iCCP'
  62. # http://www.w3.org/TR/PNG/#11iCCP
  63. v = 'a color profile\x00\x00' + v.encode('zip')
  64. else:
  65. warnings.warn('Unknown chunk type %r' % t)
  66. return t[:4],v
  67. l = map(canonical, l)
  68. # Some chunks automagically replace ones that are present in the
  69. # source PNG. There can only be one of each of these chunk types.
  70. # Create a 'replace' dictionary to record these chunks.
  71. add = []
  72. delete = set()
  73. replacing = set(['gAMA', 'sBIT', 'PLTE', 'tRNS', 'sPLT', 'IHDR'])
  74. replace = dict()
  75. for t,v in l:
  76. if v is None:
  77. delete.add(t)
  78. elif t in replacing:
  79. replace[t] = v
  80. else:
  81. add.append((t,v))
  82. del l
  83. r = png.Reader(file=inp)
  84. chunks = r.chunks()
  85. def iterchunks():
  86. for t,v in chunks:
  87. if t in delete:
  88. continue
  89. if t in replace:
  90. yield t,replace[t]
  91. del replace[t]
  92. continue
  93. if t == 'IDAT' and replace:
  94. # Insert into the output any chunks that are on the
  95. # replace list. We haven't output them yet, because we
  96. # didn't see an original chunk of the same type to
  97. # replace. Thus the "replace" is actually an "insert".
  98. for u,w in replace.items():
  99. yield u,w
  100. del replace[u]
  101. if t == 'IDAT' and add:
  102. for item in add:
  103. yield item
  104. del add[:]
  105. yield t,v
  106. return png.write_chunks(out, iterchunks())
  107. class Usage(Exception):
  108. pass
  109. def main(argv=None):
  110. import getopt
  111. import re
  112. import sys
  113. if argv is None:
  114. argv = sys.argv
  115. argv = argv[1:]
  116. try:
  117. try:
  118. opt,arg = getopt.getopt(argv, 'c:',
  119. ['gamma=', 'iccprofile=', 'sigbit='])
  120. except getopt.error, msg:
  121. raise Usage(msg)
  122. k = []
  123. for o,v in opt:
  124. if o in ['--gamma']:
  125. k.append(('gamma', float(v)))
  126. if o in ['--sigbit']:
  127. k.append(('sigbit', int(v)))
  128. if o in ['--iccprofile']:
  129. k.append(('iccprofile', open(v, 'rb').read()))
  130. if o in ['-c']:
  131. type = v[:4]
  132. if not re.match('[a-zA-Z]{4}', type):
  133. raise Usage('Chunk type must consist of 4 letters.')
  134. if v[4] == '!':
  135. k.append((type, None))
  136. if v[4] == ':':
  137. k.append((type, v[5:]))
  138. if v[4] == '<':
  139. k.append((type, open(v[5:], 'rb').read()))
  140. except Usage, err:
  141. print >>sys.stderr, (
  142. "usage: pngchunk [--gamma d.dd] [--sigbit b] [-c cHNK! | -c cHNK:text-string]")
  143. print >>sys.stderr, err.message
  144. return 2
  145. if len(arg) > 0:
  146. f = open(arg[0], 'rb')
  147. else:
  148. f = sys.stdin
  149. return chunk(sys.stdout, f, k)
  150. if __name__ == '__main__':
  151. main()