Skip to content

Instantly share code, notes, and snippets.

@RavuAlHemio
Last active December 29, 2015 12:19
Show Gist options
  • Save RavuAlHemio/7669510 to your computer and use it in GitHub Desktop.
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
#!/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