Blinking avatar fix

For general discussion about Conway's Game of Life.
Post Reply
User avatar
simsim314
Posts: 1823
Joined: February 10th, 2014, 1:27 pm

Blinking avatar fix

Post by simsim314 » May 6th, 2015, 4:03 am

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.

User avatar
Scorbie
Posts: 1692
Joined: December 7th, 2013, 1:05 am

Re: Blinking avatar fix

Post by Scorbie » May 8th, 2015, 8:24 am

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

User avatar
Scorbie
Posts: 1692
Joined: December 7th, 2013, 1:05 am

Re: Blinking avatar fix

Post by Scorbie » May 8th, 2015, 8:38 am

That does seem to work fine. Just add this line to giffer.py before the gif.close() line:

Code: Select all

gif.write('\x3B')
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?

User avatar
simsim314
Posts: 1823
Joined: February 10th, 2014, 1:27 pm

Re: Blinking avatar fix

Post by simsim314 » May 8th, 2015, 10:33 am

Scorbie wrote:By the way, simsim314, how did you make the gif with those lifehistory colors?
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.

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?

User avatar
Scorbie
Posts: 1692
Joined: December 7th, 2013, 1:05 am

Re: Blinking avatar fix

Post by Scorbie » May 8th, 2015, 10:25 pm

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?
I'll come back with this tomorrow. You can check the link I posted earler this thread in the meantime.

User avatar
Scorbie
Posts: 1692
Joined: December 7th, 2013, 1:05 am

Re: Blinking avatar fix

Post by Scorbie » May 10th, 2015, 10:20 am

Scorbie wrote:I'll come back with this tomorrow.
I'm back a little late. "Tomorrow" hasn't gone by... at my time zone... at least...
Scorbie wrote:You can check the link I posted earler this thread in the meantime.
Now I think you already checked that link.
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...

User avatar
simsim314
Posts: 1823
Joined: February 10th, 2014, 1:27 pm

Re: Blinking avatar fix

Post by simsim314 » May 10th, 2015, 3:07 pm

Do you think we can somehow skip the compression, and just use 8 codes, and then use the same site to compress?

User avatar
simsim314
Posts: 1823
Joined: February 10th, 2014, 1:27 pm

Re: Blinking avatar fix

Post by simsim314 » May 10th, 2015, 5:00 pm

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:

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()

User avatar
Scorbie
Posts: 1692
Joined: December 7th, 2013, 1:05 am

Re: Blinking avatar fix

Post by Scorbie » May 10th, 2015, 8:16 pm

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 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:Anyway I've found a workaround. I made a script that saves sequence of images,
I think there is a golly script that does that... something like savetoBMP.py?

User avatar
simsim314
Posts: 1823
Joined: February 10th, 2014, 1:27 pm

Re: Blinking avatar fix

Post by simsim314 » May 11th, 2015, 2:13 am

Scorbie wrote:I think there is a golly script that does that... something like savetoBMP.py?
I think it uses PIL which requires extra installation of a package. I used Tkinter that always comes with python.

User avatar
Scorbie
Posts: 1692
Joined: December 7th, 2013, 1:05 am

Re: Blinking avatar fix

Post by Scorbie » May 11th, 2015, 6:24 am

simsim314 wrote:I think it uses PIL which requires extra installation of a package. I used Tkinter that always comes with python.
In that case, real thanks for the script! Installing PIL was always real trouble for me...

User avatar
Andrew
Moderator
Posts: 934
Joined: June 2nd, 2009, 2:08 am
Location: Melbourne, Australia
Contact:

Re: Blinking avatar fix

Post by Andrew » May 11th, 2015, 8:16 pm

simsim314 wrote:I think it uses PIL which requires extra installation of a package.
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.

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 used Tkinter that always comes with python.
I couldn't get Tkinter to work on my Mac. Your script produces the following weird error message:

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
Use Glu to explore CA rules on non-periodic tilings: DominoLife and HatLife

User avatar
simsim314
Posts: 1823
Joined: February 10th, 2014, 1:27 pm

Re: Blinking avatar fix

Post by simsim314 » May 12th, 2015, 2:54 am

Andrew wrote:I couldn't get Tkinter to work on my Mac....
RuntimeError: Too early to create image
This message shows you have Tkinter but it works differently than on Windows. Try to add root = Tkinter.Tk() before using any Tkinter function.

And yes I see you have WriteBMP in glife - thx.

User avatar
Scorbie
Posts: 1692
Joined: December 7th, 2013, 1:05 am

Re: Blinking avatar fix

Post by Scorbie » June 5th, 2016, 4:33 pm

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<br />http://conwaylife.com/forums/viewtopic.php?f=11&amp;t=390#p2455
knightlife's LifeHistory stable glider reflector
http://conwaylife.com/forums/viewtopic.php?f=11&t=390#p2455
refl.gif (56.92 KiB) Viewed 10656 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.

Post Reply