Blinking avatar fix
Blinking avatar fix
As some of you know, your avatar created using giffer.py is annoyingly blinking in some browsers.
This is due to some unknown bug in the giffer.
I found a way to fix it: goto http://ezgif.com/speed upload your avatar, set the speed to 2 or 10 or whatever, and then download. The blinking disappears.
NOTE it's also a great place to compress your avatar more, as conwaylife.com allows only 25KB for avatar, and giffer doesn't releases the best compression.
@codeholic - I'm not sure what causing the blinking output from the giffer, but I think this is simple and fast fix.
This is due to some unknown bug in the giffer.
I found a way to fix it: goto http://ezgif.com/speed upload your avatar, set the speed to 2 or 10 or whatever, and then download. The blinking disappears.
NOTE it's also a great place to compress your avatar more, as conwaylife.com allows only 25KB for avatar, and giffer doesn't releases the best compression.
@codeholic - I'm not sure what causing the blinking output from the giffer, but I think this is simple and fast fix.
Re: Blinking avatar fix
Here's the difference of a simple blinker gif by the current giffer script and the ezgif output:
https://www.diffchecker.com/outtswil
There are four notable differences:
1) The packed field in the Logical Screen descriptor changed from 91 from F1.
That means the color resolution changed from 2bits/px to 8bits/px.
2) The Application extension (21 FF 0B 4E 45 54 53 43 41 50 45 32 2E 30 03 01 00 00 00) and
the Graphics Control Extension(21 F9 04 00 05 00 00 00) are swapped.
3) In the middle, you can see a single bit's value increased by 1. (DF to E0, D6 to D7) These govern the length of the compressed image in bits. Indeed, at the end of the "image", the 0C byte changed to 5C 00. I don't really know what this means, but this increases the length by one byte, which increases the bits' value by 1.
4) In giffer's output there is no trailer byte (0x3B, which is a semicolon)
Currently I can't figure out which of the changes causes the blinking, but I added the trailer byte to the original giffer gif file. Please check if that works. (and for other gif files too.)
As I mentioned in the giffer source code, I use this site for gif file structure:
http://www.matthewflickinger.com/lab/wh ... _bytes.asp
https://www.diffchecker.com/outtswil
There are four notable differences:
1) The packed field in the Logical Screen descriptor changed from 91 from F1.
That means the color resolution changed from 2bits/px to 8bits/px.
2) The Application extension (21 FF 0B 4E 45 54 53 43 41 50 45 32 2E 30 03 01 00 00 00) and
the Graphics Control Extension(21 F9 04 00 05 00 00 00) are swapped.
3) In the middle, you can see a single bit's value increased by 1. (DF to E0, D6 to D7) These govern the length of the compressed image in bits. Indeed, at the end of the "image", the 0C byte changed to 5C 00. I don't really know what this means, but this increases the length by one byte, which increases the bits' value by 1.
4) In giffer's output there is no trailer byte (0x3B, which is a semicolon)
Currently I can't figure out which of the changes causes the blinking, but I added the trailer byte to the original giffer gif file. Please check if that works. (and for other gif files too.)
As I mentioned in the giffer source code, I use this site for gif file structure:
http://www.matthewflickinger.com/lab/wh ... _bytes.asp
Re: Blinking avatar fix
That does seem to work fine. Just add this line to giffer.py before the gif.close() line:
And please notify me (and others) if that doesn't work.
EDIT: By the way, simsim314, how did you make the gif with those lifehistory colors?
Code: Select all
gif.write('\x3B')
EDIT: By the way, simsim314, how did you make the gif with those lifehistory colors?
Re: Blinking avatar fix
I just modfied the color table to use colors from LifeHistory, and used state 4 as 2, just added hard coded case for it where getcell is used.Scorbie wrote:By the way, simsim314, how did you make the gif with those lifehistory colors?
I tryed to make it use 3 bits for the color (92 instead of 91), but failed. Do you know how to make 8 colored gif instead of 4? What else should be added to the giffer?
Re: Blinking avatar fix
I'll come back with this tomorrow. You can check the link I posted earler this thread in the meantime.simsim314 wrote:I tryed to make it use 3 bits for the color (92 instead of 91), but failed. Do you know how to make 8 colored gif instead of 4? What else should be added to the giffer?
Re: Blinking avatar fix
I'm back a little late. "Tomorrow" hasn't gone by... at my time zone... at least...Scorbie wrote:I'll come back with this tomorrow.
Now I think you already checked that link.Scorbie wrote:You can check the link I posted earler this thread in the meantime.
I think the difference is in the "codes" in the compression algorithm.
http://www.matthewflickinger.com/lab/wh ... e_data.asp
You can see that the color codes, clear code, and EOI codes are different... But This also doesn't seem to work. Not sure why...
Re: Blinking avatar fix
Do you think we can somehow skip the compression, and just use 8 codes, and then use the same site to compress?
Re: Blinking avatar fix
Anyway I've found a workaround. I made a script that saves sequence of images, and then used the same site to make moving gif.
Here is my script:
Here is my script:
Code: Select all
import Tkinter
import golly as g
def pixel(image, pos, color):
"""Place pixel at pos=(x,y) on image, with color=(r,g,b)."""
r,g,b = color
image.put("#%02x%02x%02x" % (r,g,b), pos)
gridSize = 1
cellSize = 2
numGens = 120
directory = 'C:\\Users\\SimSim314\\Downloads\\Gifs\\'
rect = g.getselrect()
iWidth = (cellSize + gridSize) * rect[2] + gridSize
iHeight =(cellSize + gridSize) * rect[3] + gridSize
photo = Tkinter.PhotoImage(width=iWidth, height=iHeight)
colors = { 0:(0,0,0), 1:(0,255,0), 2:(0,0,255), 3:(255, 255, 255), 4:(255, 0, 0)}
for gen in xrange(1, numGens + 1):
for x in xrange(iWidth):
for y in xrange(iHeight):
gx = int(x / (cellSize + gridSize))
gy = int(y / (cellSize + gridSize))
if x % (cellSize + gridSize) < gridSize or y % (cellSize + gridSize) < gridSize:
pixel(photo, (x, y), (48,48,48))
else:
pixel(photo, (x, y), colors[g.getcell(rect[0] + gx, rect[1] + gy)])
photo.write(directory + str(gen)+'.gif', format='gif')
g.run(1)
g.show("saved to " + directory + str(gen)+'.gif')
g.update()
Re: Blinking avatar fix
I don't think we can, as the images in a gif animation should always be compressed with the lzw compression algorithm. Your workaround seems to be the best option for now. I'll see if something can be done with the giffer script.simsim314 wrote:Do you think we can somehow skip the compression, and just use 8 codes, and then use the same site to compress?
I think there is a golly script that does that... something like savetoBMP.py?simsim314 wrote:Anyway I've found a workaround. I made a script that saves sequence of images,
Re: Blinking avatar fix
I think it uses PIL which requires extra installation of a package. I used Tkinter that always comes with python.Scorbie wrote:I think there is a golly script that does that... something like savetoBMP.py?
Re: Blinking avatar fix
In that case, real thanks for the script! Installing PIL was always real trouble for me...simsim314 wrote:I think it uses PIL which requires extra installation of a package. I used Tkinter that always comes with python.
Re: Blinking avatar fix
There are 2 relevant scripts at http://www.conwaylife.com/scripts/: save-image.py requires PIL but save-bmp.py doesn't. Both only save a single file for the current pattern/selection. Below is a modification of save-bmp.py that saves a separate .bmp file for each generation.simsim314 wrote:I think it uses PIL which requires extra installation of a package.
Code: Select all
# Run the current pattern and save the selection as a separate .bmp file
# for each generation. The files are saved in the same folder as the script.
# Author: Andrew Trevorrow (andrew@trevorrow.com), March 2014.
import golly as g
from glife import validint
from glife.WriteBMP import WriteBMP
import os.path
if g.empty(): g.exit("There is no pattern.")
srect = g.getselrect()
if len(srect) == 0: g.exit("There is no selection.")
x = srect[0]
y = srect[1]
wd = srect[2]
ht = srect[3]
# prevent Python allocating a huge amount of memory
if wd * ht >= 100000000:
g.exit("Bitmap area is restricted to < 100 million cells.")
multistate = g.numstates() > 2
colors = g.getcolors() # [0,r0,g0,b0, ... N,rN,gN,bN]
state0 = (colors[1],colors[2],colors[3])
# --------------------------------------------------------------------
def CreateBMPFile(filename):
global x, y, wd, ht, multistate, colors, state0
# create 2D array of pixels filled initially with state 0 color
pixels = [[state0 for col in xrange(wd)] for row in xrange(ht)]
cellcount = 0
for row in xrange(ht):
# get a row of cells at a time to minimize use of Python memory
cells = g.getcells( [ x, y + row, wd, 1 ] )
clen = len(cells)
if clen > 0:
inc = 2
if multistate:
# cells is multi-state list (clen is odd)
inc = 3
if clen % 3 > 0: clen -= 1 # ignore last 0
for i in xrange(0, clen, inc):
if multistate:
n = cells[i+2] * 4 + 1
pixels[row][cells[i]-x] = (colors[n],colors[n+1],colors[n+2])
else:
pixels[row][cells[i]-x] = (colors[5],colors[6],colors[7])
cellcount += 1
if cellcount % 1000 == 0:
# allow user to abort huge pattern/selection
g.dokey( g.getkey() )
WriteBMP(pixels, filename)
# --------------------------------------------------------------------
# remove any existing extension from layer name
outname = g.getname().split('.')[0]
# prompt user for number of generations (= number of files)
numgens = g.getstring("The number of generations will be\n" +
"the number of .bmp files created.",
"100", "How many generations?")
if len(numgens) == 0:
g.exit()
if not validint(numgens):
g.exit('Sorry, but "' + numgens + '" is not a valid integer.')
numcreated = 0
intgens = int(numgens)
while intgens > 0:
outfile = outname + "_" + g.getgen() + ".bmp"
g.show("Creating " + outfile)
CreateBMPFile(outfile)
numcreated += 1
g.run(1)
g.update()
intgens -= 1
g.show("Number of .bmp files created: " + str(numcreated))
I couldn't get Tkinter to work on my Mac. Your script produces the following weird error message:I used Tkinter that always comes with python.
Code: Select all
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/Users/akt/Library/Application Support/Golly/golly_clip.py", line 21, in <module>
photo = Tkinter.PhotoImage(width=iWidth, height=iHeight)
File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/lib-tk/Tkinter.py", line 3284, in __init__
Image.__init__(self, 'photo', name, cnf, master, **kw)
File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/lib-tk/Tkinter.py", line 3225, in __init__
raise RuntimeError, 'Too early to create image'
RuntimeError: Too early to create image
Re: Blinking avatar fix
This message shows you have Tkinter but it works differently than on Windows. Try to add root = Tkinter.Tk() before using any Tkinter function.Andrew wrote:I couldn't get Tkinter to work on my Mac....
RuntimeError: Too early to create image
And yes I see you have WriteBMP in glife - thx.
Re: Blinking avatar fix
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: giffer.py (EDIT: Optimized for speed; x4 times faster; decent for small patterns)
+ one dependency: dialog.py
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.
Used tkinter as the GUI which I think is better.
Here's the sample LifeHistory gif working: 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()
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.")
- 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.