Last active
December 29, 2015 12:19
-
-
Save RavuAlHemio/7669510 to your computer and use it in GitHub Desktop.
noper -- adds a "nope" overlay over a PNG, JPEG or (optionally animated) GIF
This file contains hidden or 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 python3 | |
import re | |
import os, os.path | |
import tempfile | |
from subprocess import Popen, PIPE | |
""" | |
RealPopen = Popen | |
def Popen(args, *posarg, **kwarg): | |
print(args) | |
return RealPopen(args, *posarg, **kwarg) | |
""" | |
def outFileName(inFileName): | |
se = os.path.splitext(inFileName) | |
return se[0] + "-nope" + se[1] | |
def formatthem(s, l): | |
for e in l: | |
yield s.format(e) | |
def temporaryFileName(suffix=''): | |
(handle, tmpfn) = tempfile.mkstemp(suffix=suffix) | |
os.close(handle) | |
return tmpfn | |
class Image(): | |
identOutput = re.compile("^([0-9]+)x([0-9]+)$") | |
def __init__(self, fileName=None): | |
self.fileName = fileName | |
def interimExtension(self): | |
# extension for interim processing -- default is the same | |
return self.extension | |
def fetchImageData(self): | |
self.width = None | |
self.height = None | |
tmpfn = temporaryFileName(self.interimExtension()) | |
# trim transparent areas | |
Popen(["gm", "convert", "-trim", self.fileName, tmpfn]).wait() | |
p = Popen(["gm", "identify", "-format", "%wx%h", tmpfn], stdout=PIPE) | |
for lnb in p.stdout.readlines(): | |
ln = lnb.decode() | |
m = Image.identOutput.match(ln) | |
if m is None: | |
continue | |
self.width = int(m.group(1), 10) | |
self.height = int(m.group(2), 10) | |
p.wait() | |
os.remove(tmpfn) | |
def isAnimated(self): | |
# assume no | |
return False | |
def remove(self): | |
os.remove(self.fileName) | |
def overlayNope(self, nopeimg, outfn, outsize): | |
tmpfn = temporaryFileName(self.interimExtension()) | |
#Popen(["gm", "convert", "-background", "none", "-gravity", "center", "-extent", "{0}x{0}".format(outsize), infn, "tmp.gif"]).wait() | |
Popen([ | |
"convert", | |
"-background", "none", | |
"-gravity", "center", | |
"-extent", "{0}x{0}".format(outsize), | |
self.fileName, | |
tmpfn | |
]).wait() | |
Popen([ | |
"gm", | |
"composite", | |
nopeimg.fileName, | |
tmpfn, | |
outfn | |
]).wait() | |
os.remove(tmpfn) | |
return type(self)(outfn) | |
class JpegImage(Image): | |
def __init__(self, fileName=None): | |
Image.__init__(self, fileName) | |
self.extension = ".jpeg" | |
def interimExtension(self): | |
# JPEG doesn't do transparency, so defer to PNG | |
return ".png" | |
@staticmethod | |
def renderNope(width, height): | |
# JPEG doesn't support transparency, so render it as PNG | |
return PngImage.renderNope(width, height) | |
class PngImage(Image): | |
#pngFileOutput = re.compile("^PNG image data, ([0-9]+) x ([0-9]+),") | |
def __init__(self, fileName=None): | |
Image.__init__(self, fileName) | |
self.extension = ".png" | |
""" | |
def fetchImageData(self): | |
self.width = None | |
self.height = None | |
p = Popen(["file", "-b", self.fileName], stdout=PIPE) | |
for lnb in p.stdout.readlines(): | |
ln = lnb.decode() | |
m = PngImage.pngFileOutput.match(ln) | |
if m is None: | |
continue | |
self.width = int(m.group(1), 10) | |
self.height = int(m.group(2), 10) | |
p.wait() | |
""" | |
def isAnimated(self): | |
# FIXME: APNG/MNG | |
return False | |
@staticmethod | |
def renderNope(width, height): | |
#fn = "nope-{0}x{1}.png".format(width, height) | |
fn = temporaryFileName(".png") | |
Popen([ | |
"inkscape", | |
"-e", fn, | |
"-w", str(width), | |
"-h", str(height), | |
"../../signs/nope.svg" | |
]).wait() | |
return PngImage(fn) | |
class GifImage(Image): | |
giLoScr = re.compile("^ logical screen ([0-9]+)x([0-9]+)$") | |
giLoop = re.compile("^ loop (.+)$") | |
giDelay = re.compile("^ ( disposal [^ ]*)?( delay ([0-9.]+)s)?$") | |
def __init__(self, fileName=None): | |
Image.__init__(self, fileName) | |
self.extension = ".gif" | |
def fetchImageData(self): | |
self.width = None | |
self.height = None | |
self.loop = None | |
self.frameDelays = [] | |
p = Popen(["gifsicle", "-I", self.fileName], stdout=PIPE) | |
for lnb in p.stdout.readlines(): | |
ln = lnb.decode() | |
m = GifImage.giLoScr.match(ln) | |
if m is not None: | |
self.width = int(m.group(1), 10) | |
self.height = int(m.group(2), 10) | |
continue | |
m = GifImage.giLoop.match(ln) | |
if m is not None: | |
self.loop = m.group(1) | |
continue | |
m = GifImage.giDelay.match(ln) | |
if m is not None: | |
delay = m.group(3) | |
if delay is None: | |
delay = "0" | |
self.frameDelays.append(int(float(delay) * 100)) | |
continue | |
p.wait() | |
def isAnimated(self): | |
return (len(self.frameDelays) > 0) | |
@staticmethod | |
def renderNope(width, height): | |
#fn = "nope-{0}x{1}.gif".format(width, height) | |
fn = temporaryFileName(".gif") | |
# get the PNG version | |
pngNope = PngImage.renderNope(width, height) | |
# convert it to gif | |
Popen(["gm", "convert", pngNope.fileName, fn]).wait() | |
# delete the PNG | |
pngNope.remove() | |
return GifImage(fn) | |
def explode(self): | |
Popen(["gifsicle", "-U", "-e", self.fileName]).wait() | |
# return list of exploded images | |
for num in range(len(self.frameDelays)): | |
yield GifImage("{0}.{1:03}".format(self.fileName, num)) | |
@staticmethod | |
def implode(files, delays, outfn): | |
cmd = ["gifsicle", "-o", outfn, "-D", "background", "-O", "-l"] | |
for (f, delay) in zip(files, delays): | |
cmd += ["-d", str(delay), f.fileName] | |
Popen(cmd).wait() | |
return GifImage(outfn) | |
extToType = { | |
".gif": GifImage, | |
".png": PngImage, | |
".jpeg": JpegImage, | |
".jpg": JpegImage, | |
".jpe": JpegImage, | |
} | |
def nope(infn, border=0): | |
cleanups = [] | |
outframes = [] | |
# make the image object | |
ext = os.path.splitext(infn)[1] | |
inimg = extToType[ext](infn) | |
inimg.fetchImageData() | |
# render the nope image | |
dim = max(inimg.width, inimg.height) + border | |
nopeimg = inimg.renderNope(dim, dim) | |
cleanups.append(nopeimg.fileName) | |
outfn = outFileName(infn) | |
if not inimg.isAnimated(): | |
# single-frame image; take the fast path | |
inimg.overlayNope(nopeimg, outfn, dim) | |
else: | |
# explode the gif | |
explodeds = inimg.explode() | |
# overlay the nope sign | |
for exploded in explodeds: | |
frameoutfn = exploded.fileName + ".nope" | |
outframe = exploded.overlayNope(nopeimg, frameoutfn, dim) | |
outframes.append(outframe) | |
cleanups.append(exploded.fileName) | |
cleanups.append(outframe.fileName) | |
# implode the gif | |
inimg.implode(outframes, inimg.frameDelays, outfn) | |
# clean up | |
for fn in cleanups: | |
pass | |
os.remove(fn) | |
if __name__ == '__main__': | |
import sys | |
import argparse | |
parser = argparse.ArgumentParser(description="Overlay a massive \"nope\" symbol over a (possibly animated) image.") | |
parser.add_argument('filenames', metavar='IMAGE', type=str, nargs='+', help='the filenames of the images to nope') | |
parser.add_argument('-b', dest='border', metavar='BORDER', type=int, action='store', default=0, help='add a blank border of this many pixels to each side of the final image') | |
args = parser.parse_args() | |
for fn in args.filenames: | |
nope(fn, args.border) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment