build/pypng/pngchunk
author Irving Reid <irving@mozilla.com>
Wed, 04 Apr 2012 09:13:35 -0400
changeset 11568 014d81b1029f033d20453ba500b53024838e8e86
parent 8729 923b924c9422bddc54a3d8c08b765da58dddeedd
permissions -rw-r--r--
Bug 734080 - Port clang warning suppression from m-c. r=dbienvenu

#!/usr/bin/env python
# $URL: http://pypng.googlecode.com/svn/trunk/code/pngchunk $
# $Rev: 156 $
# pngchunk
# Chunk editing/extraction tool.

import struct
import warnings

# Local module.
import png

"""
pngchunk [--gamma g] [--iccprofile file] [--sigbit b] [-c cHNK!] [-c cHNK:foo] [-c cHNK<file]

The ``-c`` option is used to add or remove chunks.  A chunk is specified
by its 4 byte chunk type.  If this is followed by a ``!`` then that
chunk is removed from the PNG file.  If the chunk type is followed by a
``:`` then the chunk is replaced with the contents of the rest of the
argument (this is probably only useful if the content is mostly ASCII,
otherwise it's a pain to quote the contents, otherwise see...).  A ``<``
can be used to take the contents of the chunk from the named file.
"""


def chunk(out, inp, l):
    """Process the input PNG file to the output, chunk by chunk.  Chunks
    can be inserted, removed, replaced, or sometimes edited.  Generally, 
    chunks are not inspected, so pixel data (in the ``IDAT`` chunks)
    cannot be modified.  `l` should be a list of (*chunktype*,
    *content*) pairs.  *chunktype* is usually the type of the PNG chunk,
    specified as a 4-byte Python string, and *content* is the chunk's
    content, also as a string; if *content* is ``None`` then *all*
    chunks of that type will be removed.

    This function *knows* about certain chunk types and will
    automatically convert from Python friendly representations to
    string-of-bytes.

    chunktype
    'gamma'     'gAMA'  float
    'sigbit'    'sBIT'  int, or tuple of length 1,2 or 3

    Note that the length of the strings used to identify *friendly*
    chunk types is greater than 4, hence they cannot be confused with
    canonical chunk types.

    Chunk types, if specified using the 4-byte syntax, need not be
    official PNG chunks at all.  Non-standard chunks can be created.
    """

    def canonical(p):
        """Take a pair (*chunktype*, *content*), and return canonical
        representation (*chunktype*, *content*) where `chunktype` is the
        4-byte PNG chunk type and `content` is a string.
        """

        t,v = p
        if len(t) == 4:
            return t,v
        if t == 'gamma':
            t = 'gAMA'
            v = int(round(1e5*v))
            v = struct.pack('>I', v)
        elif t == 'sigbit':
            t = 'sBIT'
            try:
                v[0]
            except TypeError:
                v = (v,)
            v = struct.pack('%dB' % len(v), *v)
        elif t == 'iccprofile':
            t = 'iCCP'
            # http://www.w3.org/TR/PNG/#11iCCP
            v = 'a color profile\x00\x00' + v.encode('zip')
        else:
            warnings.warn('Unknown chunk type %r' % t)
        return t[:4],v

    l = map(canonical, l)
    # Some chunks automagically replace ones that are present in the
    # source PNG.  There can only be one of each of these chunk types.
    # Create a 'replace' dictionary to record these chunks.
    add = []
    delete = set()
    replacing = set(['gAMA', 'sBIT', 'PLTE', 'tRNS', 'sPLT', 'IHDR'])
    replace = dict()
    for t,v in l:
        if v is None:
            delete.add(t)
        elif t in replacing:
            replace[t] = v
        else:
            add.append((t,v))
    del l
    r = png.Reader(file=inp)
    chunks = r.chunks()
    def iterchunks():
        for t,v in chunks:
            if t in delete:
                continue
            if t in replace:
                yield t,replace[t]
                del replace[t]
                continue
            if t == 'IDAT' and replace:
                # Insert into the output any chunks that are on the
                # replace list.  We haven't output them yet, because we
                # didn't see an original chunk of the same type to
                # replace.  Thus the "replace" is actually an "insert".
                for u,w in replace.items():
                    yield u,w
                    del replace[u]
            if t == 'IDAT' and add:
                for item in add:
                    yield item
                del add[:]
            yield t,v
    return png.write_chunks(out, iterchunks())

class Usage(Exception):
    pass

def main(argv=None):
    import getopt
    import re
    import sys

    if argv is None:
        argv = sys.argv

    argv = argv[1:]

    try:
        try:
            opt,arg = getopt.getopt(argv, 'c:',
                                    ['gamma=', 'iccprofile=', 'sigbit='])
        except getopt.error, msg:
            raise Usage(msg)
        k = []
        for o,v in opt:
            if o in ['--gamma']:
                k.append(('gamma', float(v)))
            if o in ['--sigbit']:
                k.append(('sigbit', int(v)))
            if o in ['--iccprofile']:
                k.append(('iccprofile', open(v, 'rb').read()))
            if o in ['-c']:
                type = v[:4]
                if not re.match('[a-zA-Z]{4}', type):
                    raise Usage('Chunk type must consist of 4 letters.')
                if v[4] == '!':
                    k.append((type, None))
                if v[4] == ':':
                    k.append((type, v[5:]))
                if v[4] == '<':
                    k.append((type, open(v[5:], 'rb').read()))
    except Usage, err:
        print >>sys.stderr, (
          "usage: pngchunk [--gamma d.dd] [--sigbit b] [-c cHNK! | -c cHNK:text-string]")
        print >>sys.stderr, err.message
        return 2

    if len(arg) > 0:
        f = open(arg[0], 'rb')
    else:
        f = sys.stdin
    return chunk(sys.stdout, f, k)


if __name__ == '__main__':
    main()