Whoa. Added multiple color support with the availability to use custom color schemes.
Used tkinter as the GUI which I think is better.
Here's the sample LifeHistory gif working:

- knightlife's LifeHistory stable glider reflector
http://conwaylife.com/forums/viewtopic.php?f=11&t=390#p2455 - refl.gif (56.92 KiB) Viewed 10984 times
giffer.py (EDIT: Optimized for speed; x4 times faster; decent for small patterns)
Code: Select all
# Runs the selection for a given number of steps and creates a black and
# white animated GIF file.
# Based on giffer.pl, which is based on code by Tony Smith.
import golly as g
import os
import struct
from collections import namedtuple
from dialog import getstrings
########################################################################
# Color schemes
########################################################################
ColorScheme = namedtuple('ColorScheme', 'size table')
"""Color scheme used in the gif file.
ColorScheme is merely a container for these two things:
size: Contains a "color table size" that goes to the logical screen
descriptor.
**Note**
The number of colors used in the gif file is: 2 ** (size + 1).
table: Contains a table of the 2**(size+1) colors. As you can see
below, the colors are represented as RGB, each color in which
occupying 1 byte (values 0-255.) The 3-byte colors are simply
concatenated to make the color table.
**Note**
The last color is used as the borderline color in this script.
"""
lifewiki = ColorScheme(size=1, table= (
"\xFF\xFF\xFF" # State 0: white
"\x00\x00\x00" # State 1: black
"\x00\x00\x00" # (ignored)
"\xC6\xC6\xC6" # Boundary: LifeWiki gray
))
lifehistory = ColorScheme(size=2, table=(
"\x00\x00\x00" # State 0: black
"\x00\xFF\x00" # State 1: green
"\x00\x00\x80" # State 2: dark blue
"\xD8\xFF\xD8" # State 3: light green
"\xFF\x00\x00" # State 4: red
"\xFF\xFF\x00" # State 5: yellow
"\x60\x60\x60" # State 6: gray
"\x00\x00\x00" # Boundary color
))
# Edit this to set the color scheme.
colors = lifehistory
########################################################################
# Parsing inputs
########################################################################
# Sanity check
rect = g.getselrect()
if rect == []:
g.exit("Nothing in selection.")
[rectx,recty,width,height] = rect
if(width>=65536 or height>=65536):
g.exit("The width or height of the GIF file must be less than 65536 pixels.")
def parseinputs():
global gens
global fpg
global pause
global purecellsize # Cell size without borders
global cellsize # Cell size with borders
global gridwidth # Border width
global vx
global vy
global filename
global canvaswidth
global canvasheight
# Get params
gens, fpg, pause, purecellsize, gridwidth, v, filename = getstrings(entries=[
("Number of generations for the GIF file to run:", "4"),
("The number of frames per generation (1 for statonary patterns):", "1"),
("The pause time of each generation in centisecs:", "50"),
("The size of each cell in pixels:", "14"),
("The width of gridlines in pixels:", "2"),
("The offset of the total period:", "0 0"),
("The file name:", "out.gif")
])
# Sanity check params
def tryint(var, name):
try:
return int(var)
except:
g.exit("{} is not an integer: {}".format(name, var))
try:
vx, vy = v.split()
except:
g.exit("You should enter the speed as {x velocity} {y velocity}.\n"
"ex1) 0 0\t ex2) -1 3")
gens = tryint(gens, "Number of gens")
pause = tryint(pause, "Pause time")
purecellsize = tryint(purecellsize, "Cell size")
gridwidth = tryint(gridwidth, "Grid width")
vx = tryint(vx, "X velocity")
vy = tryint(vy, "Y velocity")
fpg = tryint(fpg, "Frames per gen")
pause //= fpg
cellsize = purecellsize + gridwidth
canvasheight = cellsize*height + gridwidth
canvaswidth = cellsize*width + gridwidth
if(canvaswidth>=65536 or canvasheight>=65536):
g.exit("The width or height of the GIF file must be less than 65536 pixels. "
"Received width: {}, height: {}".format(canvaswidth, canvasheight))
########################################################################
# GIF formatting
# Useful information on GIF formats in:
# http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
########################################################################
def makegif():
header, trailer = "GIF89a", '\x3B'
screendesc = struct.pack("<2HB2b", canvaswidth, canvasheight,
0x90+colors.size, 0, 0)
applic = "\x21\xFF\x0B" + "NETSCAPE2.0" + struct.pack("<2bHb", 3, 1, 0, 0)
imagedesc = "\x2C" + struct.pack("<4HB", 0, 0, canvaswidth, canvasheight, 0x00)
bordercolor = 2 ** (colors.size + 1) - 1
borderrow = [bordercolor] * (canvaswidth + cellsize)
# Gather contents to write as gif file.
gifcontent = [header, screendesc, colors.table, applic]
for f in xrange(gens*fpg):
# Graphics control extension
gifcontent += ["\x21\xF9", struct.pack("<bBH2b", 4, 0x00, pause, 0, 0)]
# Get data for this frame
dx = int(vx * f * cellsize // (fpg * gens))
dy = int(vy * f * cellsize // (fpg * gens))
dx_cell, dx_px = divmod(dx, cellsize)
dy_cell, dy_px = divmod(dy, cellsize)
# Get cell states (shifted dx_cell, dy_cell)
# The bounding box is [rectx+dx_cell, recty+dy_cell, width+1, height+1]
cells = []
# The image is made of cell rows (height purecellsize) sandwiched
# by border rows (height gridwidth).
for y in xrange(recty+dy_cell, recty+dy_cell+height+1):
cells += [borderrow] * gridwidth
row = []
# Each row is made of cell pixels (width purecellsize)
# sandwiched by border pixels (width gridwidth)
for x in xrange(rectx+dx_cell, rectx+dx_cell+width+1):
row += [bordercolor] * gridwidth
row += [g.getcell(x, y)] * purecellsize
row += [bordercolor] * gridwidth
cells += [row] * purecellsize
cells += [borderrow] * gridwidth
#g.setclipstr('\n'.join(str(row) for row in cells).replace(', ', ''))
#g.note('')
# Cut a canvaswidth x canvasheight image starting from dx_px, dy_px.
newcells = [row[dx_px:dx_px+canvaswidth] for row in
cells[dy_px:dy_px+canvasheight]]
image = ''.join(''.join(chr(i) for i in row) for row in newcells)
# Image descriptor + Image
gifcontent += [imagedesc, compress(image, colors.size+1)]
g.show("{}/{}".format(f+1, gens*fpg))
if (f % fpg == fpg - 1):
g.run(1)
g.update()
gifcontent.append(trailer)
with open(os.path.join(os.getcwd(), filename),"wb") as gif:
gif.write("".join(gifcontent))
g.show("GIF animation saved in {}".format(filename))
########################################################################
# GIF compression
# Algorithm explained in detail in:
# http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp
########################################################################
def compress(data, mincodesize):
"""Apply lzw compression to the given data and minimum code size."""
ncolors = 2**mincodesize
cc, eoi = ncolors, ncolors + 1
table = {chr(i): i for i in xrange(ncolors)}
codesize = mincodesize + 1
newcode = ncolors + 2
outputbuff, outputbuffsize, output = cc, codesize, []
databuff = ''
for next in data:
newbuff = databuff + next
if newbuff in table:
databuff = newbuff
else:
table[newbuff] = newcode
newcode += 1
# Prepend table[databuff] to outputbuff (bitstrings)
outputbuff += table[databuff] << outputbuffsize
outputbuffsize += codesize
databuff = next
if newcode > 2**codesize:
if codesize < 12:
codesize += 1
else:
# Prepend clear code.
outputbuff += cc << outputbuffsize
outputbuffsize += codesize
# Reset table
table = {chr(i): i for i in xrange(ncolors)}
newcode = ncolors + 2
codesize = mincodesize + 1
while outputbuffsize >= 8:
output.append(outputbuff & 255)
outputbuff >>= 8
outputbuffsize -= 8
outputbuff += table[databuff] << outputbuffsize
outputbuffsize += codesize
while outputbuffsize >= 8:
output.append(outputbuff & 255)
outputbuff >>= 8
outputbuffsize -= 8
output.append(outputbuff)
# Slice outputbuff into 255-byte chunks
words = []
for start in xrange(0, len(output), 255):
end = min(len(output), start+255)
words.append(''.join(chr(i) for i in output[start:end]))
contents = [chr(mincodesize)]
for word in words:
contents.append(chr(len(word)))
contents.append(word)
contents.append('\x00')
return ''.join(contents)
########################################################################
# Main
########################################################################
def main():
parseinputs()
makegif()
main()
+ one dependency: dialog.py
Code: Select all
import golly as g
from Tkinter import *
import ttk
class StringsDialog(ttk.Frame):
"""A dialog window that can get a multiple string responses."""
def __init__(self, master, entries, width=10):
ttk.Frame.__init__(self, master)
self.grid(column=0, row=0, sticky=(N, W, E, S))
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
# Response data
self.responses = []
self.respentries = []
# Build the items in the window
for index, entry in enumerate(entries):
try:
prompt, initial = entry
if initial is None:
initial = ''
except:
raise TypeError('Each prompt should contain a prompt and '
'an initial value!')
ttk.Label(self, text=prompt).grid(column=0, row=index)
resp = ttk.Entry(self, width=width)
resp.grid(column=1, row=index)
resp.insert(0, initial)
self.respentries.append(resp)
self.responses.append(initial)
ttk.Button(self, text="OK",
command=self.getresponses).grid(column=0, row=index+1)
ttk.Button(self, text="Cancel",
command=self.master.destroy).grid(column=1, row=index+1)
def getresponses(self):
"""Get all the responses and close the window."""
self.responses = [resp.get() for resp in self.respentries]
self.master.destroy()
class BoolDialog(ttk.Frame):
"""A dialog window that can get a boolean response."""
def __init__(self, master, prompt):
ttk.Frame.__init__(self, master)
self.grid(column=0, row=0, sticky=(N, W, E, S))
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
ttk.Label(self, text=prompt).grid(column=0, row=0)
ttk.Button(self, text="Yes", command=self.settrue).grid(column=0, row=1)
ttk.Button(self, text="No", command=self.setfalse).grid(column=1, row=1)
self.response = False
def settrue(self):
"""Set the response to True and close the window."""
self.response = True
self.master.destroy()
def setfalse(self):
"""Set the response to False and close the window."""
self.response = False
self.master.destroy()
def getstrings(entries, title='', width=10):
"""Return the responses with the given entries.
<<Arguments>>
entries -- list of (prompt, initial value) pairs.
title -- the tile text of the window
width -- the width of the entry box
"""
root = Tk()
root.title(title)
sd = StringsDialog(root, entries, width)
root.mainloop()
return sd.responses
def getbool(prompt, title=''):
"""Return the user's choice as a boolean."""
root = Tk()
root.title(title)
bd = BoolDialog(root, prompt)
root.mainloop()
return bd.response
if __name__ == '__builtin__':
# getstrings test
strings = getstrings(
entries=[
('Prompt 1', 'I like'),
('Prompt 2', 'golly and'),
('Prompt 3', 'Python!')
],
title="getstrings test",
width=30
)
g.note("This is the input received:\n{}".format(strings))
# getbool test
boolean = getbool("Do you like GollyGUI?", "getbool test")
if boolean:
g.note("Thanks!")
else:
g.note("I'll try to make it better.")
Known issues (Somebody help me with dialog.py please!)
- the search goes on even if you cancel or close the dialog.
- The texts are aligned at the center, and it is unpretty.
- The yes/no are aligned with the getstrings, which isn't pretty.
- (Very quirky) In 32bit linux, the dialog's position changes everytime and is really strange.