-
-
Save Bidski/63f0f4f070762567ec88f576bf08c8c3 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).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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