Skip to content

Instantly share code, notes, and snippets.

@dankrause
Created December 21, 2012 18:34
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save dankrause/4354803 to your computer and use it in GitHub Desktop.
Save dankrause/4354803 to your computer and use it in GitHub Desktop.
A command-line tool that manipulates png meta-data in a very Unix-like way. Requires the Python Imaging Library (PIL).
#!/usr/bin/env python
import json, optparse, sys
try:
import PIL.Image, PIL.PngImagePlugin
except:
print >> sys.stderr, "Unable to import Python Imaging Library. Please ensure that it is installed."
sys.exit(1)
# PIL.PngImagePlugin.PngStream mixes these keys in with meta-data, even though they're not from tEXt or zTXt chunks.
reserved_keys = ('interlace', 'gamma', 'dpi', 'transparency', 'aspect', 'icc_profile')
class MyParser(optparse.OptionParser):
def format_epilog(self, formatter):
return "".join(formatter.format_epilog(paragraph) for paragraph in self.epilog)
version = "%prog 1.4"
usage = "usage: %prog [options] pngfile [pngfile ...]"
description = "Edits and displays arbitrary meta-data in one or more png files. Requires the Python Imaging Library (PIL)."
epilog = [
"This tool ignores the following reserved keys: (%s)." % ', '.join(reserved_keys),
"This tool can't differentiate between values stored in tEXt and zTXt chunks when reading.",
"Most limitations of this tool are inherited from the PngStream class of the PngImagePlugin module in the Python Imaging Library."]
parser = MyParser(version=version, usage=usage, description=description, epilog=epilog)
readinggroup = optparse.OptionGroup(parser, "Reading meta-data", "All meta-data key=value pairs in all png files are output by default.")
readinggroup.add_option("-q", "--quiet", dest="quiet", default=False, action="store_true", help="Supress per-file headers and errors.")
readinggroup.add_option("-s", "--silent", dest="silent", default=False, action="store_true", help="Supress all output.")
readinggroup.add_option("-g", "--get", dest="get", metavar="KEY", default=None, action="append", help="Retrieve the value of a single meta-data key. Can be used multiple times.")
removegroup = optparse.OptionGroup(parser, "Removing meta-data", "Options that delete meta-data will be applied before options that add or change meta-data.")
removegroup.add_option("-c", "--clear", dest="clear", default=False, action="store_true", help="Clear all png meta-data in the file.")
removegroup.add_option("-d", "--delete", dest="delete", metavar="KEY", default=None, action="append", help="Delete a single meta-data key. Can be used multiple times.")
updategroup = optparse.OptionGroup(parser, "Updating meta-data")
updategroup.add_option("-f", "--file", dest="file", default=None, help="Read meta-data key=value pairs from a file, one per line. Use - to read from stdin.")
updategroup.add_option("-k", "--key", dest="keys", metavar="META", default=None, action="append", help="Add or update a single meta-data key, using a \"key=value\" pair. Can be used multipe times.")
updategroup.add_option("", "--force-save", dest="force", default=False, action="store_true", help="Re-save the png file, even if no key changes are made. Useful for converting meta-data from one chunk-type to another.")
formatgroup = optparse.OptionGroup(parser, "Format options")
formatgroup.add_option("-z", "--ztxt", dest="ztxt", default=False, action="store_true", help="Store meta-data using a zTXt (compressed) chunk instead of the defaut tEXt chunk. (Note: all existing meta-data will be moved to the chosen format)")
formatgroup.add_option("-j", "--json", dest="json", default=False, action="store_true", help="Handle file input and command output as JSON instead of \"key=value\" pairs.")
parser.add_option_group(readinggroup)
parser.add_option_group(removegroup)
parser.add_option_group(updategroup)
parser.add_option_group(formatgroup)
parser.formatter.max_help_position = 26
(options, args) = parser.parse_args()
if len(args) == 0: parser.error("You must specify at least one png file. Use -h for help.")
if options.silent:
if options.get is not None: parser.error("Using --get with --silent has no effect.")
if not options.clear and options.delete is None and options.file is None and options.keys is None and not options.ztxt:
parser.error("No operations specified. Use -h for help.")
# A couple convenience functions
def parse_pairs(pairs):
for pair in pairs:
key, sep, val = pair.partition('=')
if sep == '=':
yield (key.strip(), val.strip())
else:
print >> sys.stderr, "Parsing error: %p" % pair
def decode_if_json(val):
try:
return json.loads(val)
except:
return val
# Handle file input (or stdin)
filekeys = {}
if options.file is not None:
if options.file == '-':
infile = sys.stdin
else:
try:
infile = open(options.file)
except IOError as e:
print >> sys.stderr, "Error: Unable to read input file %s: %s" % (options.file, e[1])
sys.exit(1)
if options.json:
for key, val in json.load(infile).iteritems():
key=str(key)
if key in reserved_keys: continue
if val.__class__ in (unicode, int, float, bool):
filekeys[key] = val
else:
filekeys[key] = str(json.dumps(val))
else:
filekeys = dict(parse_pairs(infile.readlines()))
if infile is not sys.stdin: infile.close()
# Loop through png files and process each one according to options
exitcode = 0
for filename in args:
try:
image = PIL.Image.open(filename)
info = dict([(k,v) for k, v in image.info.iteritems() if k not in reserved_keys])
except IOError as e:
if len(e.args) > 1:
msg = e[1]
else:
msg = e[0]
if not options.quiet: print >> sys.stderr, "\nError: Unable to read png file %s: %s" % (filename, msg)
exitcode = 1
continue
dirty = False
if options.clear:
dirty = True
info.clear()
if options.delete is not None and not options.clear:
for key in options.delete:
if key in info:
dirty = True
del(info[key])
if options.file is not None:
dirty = True
info.update(filekeys)
if options.keys is not None:
dirty = True
info.update(parse_pairs(options.keys))
if options.get is not None:
for key in options.get:
if options.json:
print decode_if_json(info[key])
else:
print info[key]
if dirty or options.force:
meta = PIL.PngImagePlugin.PngInfo()
for k,v in info.iteritems():
if k not in reserved_keys: meta.add_text(k, v, options.ztxt)
try:
image.save(filename, "PNG", pnginfo=meta)
except IOError as e:
print >> sys.stderr, "Unable to save %s: %s" % (filename, e[1])
exitcode = 1
continue
if not options.silent and options.get is None:
if not options.quiet and len(args) > 1: print "\n --- %s --- " % filename
if options.json:
print json.dumps(dict([(key, decode_if_json(val)) for key, val in info.iteritems()]))
else:
for meta in info.iteritems():
print "%s=%s" % meta
sys.exit(exitcode)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment