Page 1 of 1

Life file format converters and display programs

Posted: July 6th, 2009, 6:40 am
by PM 2Ring
A few months ago, I downloaded Mark D. Niemiec's RLE2TXT, a C program that converts RLE files to "dot star" and other simple ASCII formats. Unfortunately, a logic bug causes this program to make errors when decoding some patterns, for example this simple glider-eater combination. :(
Image
Luckily, I was able to track down this bug. :) Here's the repaired version. This program has been tested on Linux, but should compile properly on any platform. See Mark's homepage for the original version.

Code: Select all

/*
 * Convert Life pattern from .RLE format to text format
 * Implemented in Borland C 3.1 for MS-DOS
 * (This should be easily convertible to other platforms)
 *
 * By Mark D. Niemiec 1997
 * E-mail:  mniemiec@interserv.com
 * WWW:  http://home.sprynet.com/interserv/mniemiec/lifepage.htm
 */
 
 /* 
 Minor logic bug repaired by PM 2Ring, March 2009.

 XXXX Needs work: lines should be padded to pattern width   
 */
 

#include <stdio.h>        // ferror fopen fprintf getc putc sprintf ungetc
#include <stdlib.h>        // exit
#include <string.h>        // strcat strlen strncpy strnicmp

/*
 * Global variables
 */
char    outputChars[7] = ".OXYZa"; /* dead state, 5 living states */

/*
 * .RLE files are ASCII text files with a one-line header,
 * and followed by a variable-length run-length encoded image.
 *
 * The header may be preceded by any number of blank lines and/or
 * comment lines beginning with a pound sign '#'.
 *
 * The header consists of several fields separated by commas,
 * and ending in a newline.
 * The first two fields are "x = nnn" and "y = nnn", (where nnn
 * is an integer) respectively specifying the pattern width and height.
 * These fields are required.
 *
 * An optional rule field, of the form "rule = Bnnn/Snnn" may follow,
 * encoding totalistic rules.  By convention, this field is required
 * a rule different from Life (B3/S23) is used.
 *
 * The data portion consists of one or more runs, terminated by
 * an exclamation mark '!'.
 * The runs are defined by a single character, preceded by an optional
 * integer repeat factor (which defaults to 1 if omitted).
 * The character may be one of the following:
 *    $    Newline
 *    b    Dead cell
 *    o    Living cell, or first living state in multi-state Life
 *    x*    Second living state in multi-state Life
 *    y*    Third living state in multi-state Life
 *    z*    Fourth living state in multi-state Life
 *    other    Unrecognized codes should be interpreted as living cells
 * (NOTE: characters marked * above are extensions used by this program,
 *  but not part of the generally accepted de-facto RLE standard;
 *  however, x, y, and z behave correctly when treated as 'other')
 * Embedded white space is ignored.
 *
 * The de-facto RLE standard adds the following additional stipulations:
 * Lines should not exceed 70 characters to avoid confusing brain-damaged
 * e-mail programs, so newlines should be added as required.
 * Newlines and other white-space may not be inserted in the middle of a
 * repeat factor, nor between a character and its repeat factor.
 * This program can, however, read files which violate these stipulations.
 *
 * This program can also read .RLE files without headers,
 * since it does not use the header information for anything.
 */


/*
 * Read .RLE-format file
 */
void
ReadRle (FILE *srcFile, FILE *dstFile)
{
    int        repl;        /* replication count */
    int        ifrepl;        /* is replication count given? */
    int        c;        /* character read from file */
    int        curx;        /* x coordinate being written */
    int        cury;        /* y coordinate being written */

    for (;;) {            /* strip leading comment lines: */
        switch ((c = getc (srcFile))) {
        case EOF:            /* end of file: no comment */
            break;
        case '\n':            /* blank line: ignored */
            continue;
        case '#':            /* #: comment line */
            while ((c = getc (srcFile)) != '\n') {
                if (c == EOF)    /* EOF in comment!?! */
                    break;
            }
            continue;
        default:            /* other: legitimate text */
            ungetc (c, srcFile);
            break;
        }
        break;
    }

    while ((c = getc(srcFile)) == ' ' || c == '\t' || c == '\n');
    if (c == 'x') {            /* header line is supplied (and ignored) */
        while ((c = getc(srcFile)) != EOF && c != '\n');
    } else {            /* no header: translator doesn't care */
        ungetc (c, srcFile);
    }

    for (repl = 1, ifrepl = 0, curx = cury = 0; ; ) {
        while ((c = getc (srcFile)) == ' ' || c == '\t' || c == '\n')
          ;
        //fputc (c, stderr);
        switch (c) {
        default:
            if (c >= '0' && c <= '9') {
                repl = (ifrepl ? 10*repl : 0) + (c-'0');
                ifrepl = 1;
                continue;
            } else {
                outputChars[5] = c;
                c = 5;
                break;
            }
        case EOF:
            fprintf (stderr, "Missing end-of-file '!' mark\n");
            break;
        case '!':
            if (curx != 0)        /* ALWAYS flush last line */
                fputc ('\n', dstFile);
            return;
        case 'b':
            c = 0;
            break;
        case 'o':
            c = 1;
            break;
        case 'x':
            c = 2;
            break;
        case 'y':
            c = 3;
            break;
        case 'z':
            c = 4;
            break;
        case '$':
            for (; repl > 0; ++cury, --repl, curx = 0)
                fputc ('\n', dstFile);
            repl = 1;
            ifrepl = 0;
            continue;
        }
        
        fprintf(stderr, "%d:%d %c\n", repl, c, outputChars[c]);
        for (c = outputChars[c]; repl > 0; ++curx, --repl)
            fputc (c, dstFile);

        repl = 1;
        ifrepl = 0;
    }
}


/*
 * Main
 */
int
main (int argc, char **argv)
{
    char        *arg;        /* argument being examined */
    FILE        *srcFile;    /* source file */
    FILE        *dstFile;    /* destination file */

    if (argc > 1 && strncmp (argv[1], "-c", 2) == 0) {
        strncpy (outputChars, argv[1]+2, 5);
        --argc;
        ++argv;
    }

    if (argc != 3) {
        fprintf (stderr,
          "Usage:  rle2txt [-c[.[o[x[y[z]]]]]] infile.rle outfile.txt\n");
        exit (1);
    }

    if (outputChars[0] == '\0')
        strcat (outputChars, ".");
    if (outputChars[1] == '\0')
        strcat (outputChars, "*");
    while (strlen (outputChars) < 5) {
        sprintf (outputChars + strlen (outputChars),
          "%c", outputChars[1]);
    }

    if ((srcFile = fopen (argv[1], "r")) == NULL) {
        fprintf (stderr, "Cannot open %s\n", argv[1]);
        exit (1);
    }

    if ((dstFile = fopen (argv[2], "w")) == NULL) {
        fprintf (stderr, "Cannot create %s\n", argv[2]);
        exit (1);
    }

    ReadRle (srcFile, dstFile);

    if (ferror (dstFile)) {
        fprintf (stderr, "Disk full writing to %s\n", argv[2]);
        exit (1);
    }

    return 0;
}
Here's the documentation:

Code: Select all

NAME
    RLE2TXT - Life RLE file conversion utility,  by Mark D. Niemiec

SYNOPSIS
    RLE2TXT [-C[.[o[x[y[z]]]]]] infile.rle outfile.txt

DESCRIPTION
    RLE2TXT converts a Life file in RLE format into a plain text file.
    By default, dead cells are displayed as periods, while living cells
    are displayed as Os.  If multiple living states exist in the RLE
    file, the first four such states are displayed as O, X, Y, and Z.
    Other codes in the RLE file are displayed as-is without translation.

    The output characters can be overridden by the -C command:
      -C       displays dead cells as ., and all living cells to *,*,*,*
      (which is commonly used by many other Life programs)
      -Cd      displays dead cells as d, and all living cells as *,*,*,*
      -Cdo     displays dead cells as d, and all living cells as o,o,o,o
      -Cdox    displays dead cells as d, and all living cells as o,x,o,o
      -Cdoxy   displays dead cells as d, and all living cells as o,x,y,o
      -Cdoxyz  displays dead cells as d, and all living cells as o,x,y,z
      -C.OXYZ  is the default, if no -C command is given.

    If the .RLE file contains a header, it is read but ignored.

PORTABILITY
    Requires DOS 2.0 or higher.
    Source code should be Unix compatible with only trivial changes.

BUGS
    There is no way to output spaces for dead cells.
    Does not support files with runs larger than 32767 (but then,
    most other programs don't handle 32768-character lines well either.)

Re: Life file format converters and display programs

Posted: July 6th, 2009, 7:03 am
by PM 2Ring
RLE file viewer

This Python program is a fairly basic RLE file viewer. It uses the GTK library to create the display & handle the user interface. It is a standalone program, not a Golly script. I wrote it to make it possible to easily view RLE files in a file manager (like Konqueror, the KDE file manager). It's not very fast, but for smaller patterns it's faster than starting up Golly, and rather more convenient than dragging files from the file manager into Golly when you're scanning through a large folder looking for a particular pattern, IMHO.

This program allows you to zoom in & out and to change colours, but it doesn't do any Life calculations. If you want to see patterns evolve, use Golly. :)

Most of the key commands are the same as Golly's. Press 'h' for help to see the complete list.

This program is a work in progress (as you may be able to tell from the debugging print statements :)). Any comments and suggestions are welcome (as always). I'm thinking of adding the ability to save the pattern as an image file (GTK has support for several formats built in) and possibly the ability to load images & save as RLE, although it's probably a more sensible option to use Golly (or the Life editor of your choice) for that. Besides, this was supposed to be a small program, but it's getting much larger than I originally expected. :)

Patterns can be read from a file, a string on the commandline, or standard input. When standard input is the source, you can use standard *nix readline-style line editing (if the readline module is available).

With some minor editing, this script can also be used to convert RLE data to simple ASCII formats. If anyone needs it, I can post a simpler non-GUI version that just does such conversions.

Code: Select all

#!/usr/bin/env python

''' Game of Life RLE format decoder & viewer
        
    Usage: python LifeRLE.py [source]
    where source may be a file name or an RLE string.
    If no arg, source is Standard Input.
    Supports readline-style line editing on Standard Input. 
'''

import sys, os.path, readline, shlex

import pygtk
pygtk.require('2.0')
import gtk

#Screen size
MAXWIDTH = 1024 - 30
MAXHEIGHT = 768 - 30

#Debugging output level. 0=no debugging
verbosity = 0 

#Debug output routine                
def printe(v, s):
    ''' Print to stderr '''
    if v <= verbosity:
        print >>sys.stderr, s
        
def decodeRLE(src):
    ''' Decode RLE to a bitmap list of lists of int bits '''
    bitmap = []

    #Set up lexer. 
    lex = shlex.shlex(src)
    #Treat numbers, 'rule' and all other single chars as tokens
    lex.wordchars = '0123456789rule'
    #Ignore comma, equals, slash and whitespace
    lex.whitespace += ',=/'
    #lex.whitespace_split = False
    lex.eof = '!'

    width = maxwidth = height = 0
    row = []
    pat = []
    rep = 1
    
    #Read & convert data
    for tt in lex:
        if not tt: 
            break
        printe(4, 'Token: ' + repr(tt))
        
        if tt == 'x':
            width = int(lex.get_token())
            printe(1, 'width=%d' % width)
            
        elif tt == 'y':
            height = int(lex.get_token())
            printe(1, 'height=%d' % height)
               
        elif tt == 'rule':
            for i in (0,1):
                rt = lex.get_token()
                if rt == 'B':
                    birth = lex.get_token()
                    printe(1, 'birth=%s' % birth)
                elif rt == 'S':     
                    survive = lex.get_token()
                    printe(1, 'survive=%s' % survive)
                else:
                    printe(0, 'Bad rule type: %s. Aborting' % rt) 
                    return   
                
        elif tt == 'b':
            row += rep * [0]
            rep = 1
             
        elif tt == 'o':
            row += rep * [1]
            rep = 1
            
        elif tt == '$':
            pat += [row]
            w = len(row)
            maxwidth = max(maxwidth, w)
            printe(3, 'row=%s width=%d' % (row, w))
            rep -= 1
            if rep>0:
                pat += rep * [[0]]                       
                printe(3, 'blanks=%d' % rep)
            row = []
            rep = 1
            
        else:            
            rep = int(tt)

    #Flush last row            
    if row:
        pat += [row]                       
        printe(3, 'row=%s' % row)
        
    printe(1, '')
    
    #Add rows if row count < height, or change height to match if row count > height
    h = len(pat)
    printe(1, 'height: %d, Actual height: %d' % (height, h))
    if h < height:                
        pat += (height - h) * [[0]]
    else:
        height = h            

    printe(1, 'width: %d, Actual width: %d' % (width, maxwidth))
    if width < maxwidth:        
        width = maxwidth    
    
    #Pad rows to width        
    for row in pat:
        w = len(row)
        if w < width:
            row += (width - w) * [0]

    if 2 <= verbosity:             
        for row in pat: 
            printe(2, ''.join(['.o'[i] for i in row]))
        
    return width, height, pat
      
class myline:
    ''' Read from Standard Input, with line editing '''
    s = ''
    def read(self, n):
        ''' Get the next n chars '''
        while n > len(self.s):
            try:
                b = raw_input()
            except EOFError:
                return ''
            self.s += b + '\n'
        a, self.s = self.s[:n], self.s[n:]
        return a

    def readline(self):
        ''' Get the next line '''
        if 0 == len(self.s):
            try:
                b = raw_input()
            except EOFError:
                return ''
            self.s += b + '\n'
        a, self.s = self.s, ''
        return a
    
class PixmapImage:
    def __init__(self, title):
        # Create the main window
        window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        # Attach delete_event signal to terminate program
        window.connect("destroy", lambda w: gtk.main_quit())
        window.connect("key_release_event", self.key_event)        
        window.add_events(gtk.gdk.KEY_RELEASE_MASK)
        window.set_title(title)
        window.set_border_width(1)
        window.show()
        self.window = window
        
        # An Image widget to contain the pixmap
        self.image = gtk.Image()
        self.image.show()     

        # A Scrolled Window to hold the Image
        self.sw = gtk.ScrolledWindow()
        self.sw.add_with_viewport(self.image)        
        self.sw.connect("button_press_event", self.button_press_event)        
        
        #self.sw.connect("scroll_event", self.wheel_scroll_event)
        #self.sw.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.SCROLL_MASK)  
        self.sw.add_events(gtk.gdk.BUTTON_PRESS_MASK)  
        self.sw.show()
        window.add(self.sw)   
        
        #Color palette & dialog
        self.colors = ['#000000000000', '#FFFFFFFFFFFF']          
        self.colorseldlg = None
                      
    def set_bitmap(self, width, height, bscale, minbscale, pat):
        self.width = width
        self.height = height
        self.bscale = bscale
        self.minbscale = minbscale
        self.pat = pat        
        
    def show_image(self, reuse=False):
        if not reuse:
            # Convert bitmap data to XPM format
            self.xwidth, self.xheight, self.xpm = BMtoXPM(self.width, self.height, 
                self.pat, self.bscale)

        xpmhdr = ['%d %d 2 1' % (self.xwidth, self.xheight),
            '0 c %s' % self.colors[0], '1 c %s' % self.colors[1]]  
          
        # Create the pixmap from XPM data
        pixmap, mask = gtk.gdk.pixmap_create_from_xpm_d(self.window.window, None, xpmhdr + self.xpm)
        self.image.set_from_pixmap(pixmap, mask)
        if self.xwidth<MAXWIDTH or self.xheight<MAXHEIGHT:
            self.window.window.resize(self.xwidth+30, self.xheight+30)

    def adjust_scale(self, delta):
        if delta==0:
            b = 0
        else:
            b = self.bscale + delta
            b = max(min(b, 4), self.minbscale)            
        if b != self.bscale:
            self.bscale = b            
            self.show_image()               

  # Some instructions
    def help(self):
        dialog = gtk.Dialog("Life RLE Help", self.window, gtk.DIALOG_NO_SEPARATOR)
        label = gtk.Label("""
    Game of Life RLE file viewer. 
    Created by PM 2Ring, June 2009.
 
    Mouse:   
    Left click - zoom in.
    Right click - zoom out.
    Middle click - no scaling.
    
    Keys:
    ], * - zoom in.
    [, / - zoom out.
    1, Enter, Return - no scaling.
    
    c - select foreground color
    C - select background color
    b - invert color scheme
    
    h - this help window  
    ESC, q: quit
""")
        dialog.vbox.pack_start(label, expand=False)
        label.show()
        dialog.show()
                
    def key_event(self, widget, event=None):
        key = event.keyval<256 and chr(event.keyval) or event.keyval
        print key, event.type, event.keyval
        
        if key == 'b':
            #Invert color scheme
            self.colors = self.colors[::-1]
            self.show_image(True) 
        elif key in (']', '*', 65450):
            #Zoom in
            self.adjust_scale(1)
        elif key  in ('[', '/', 65455):
            #Zoom out
            self.adjust_scale(-1)
        elif key in ('1', 65457, 65421, 65293):
            #Return, Enter or 1: No scaling
            self.adjust_scale(0) 
        elif key == 'h':
            self.help()
            
        elif key == 'c':
            self.change_color(1, 'Live Cell color')
        elif key == 'C':
            self.change_color(0, 'Dead Cell color')
             
        elif key == 'q' or key == 65307:
            #Escape or q to quit            
            gtk.main_quit()           
        return True
            
    def change_color(self, colornum, title=''):
        color = gtk.gdk.color_parse(self.colors[colornum])
        newcolor = self.choose_color(color, title)
        if newcolor != color:
            self.colors[colornum] = colortext(newcolor)
            #print colortext(color)
            self.show_image(True) 
            
    def choose_color(self, color, title=''):
        # Create color selection dialog
        if self.colorseldlg == None:
            self.colorseldlg = gtk.ColorSelectionDialog('')
        self.colorseldlg.set_title(title)            

        # Get the ColorSelection widget
        colorsel = self.colorseldlg.colorsel
        colorsel.set_current_color(color)
        colorsel.set_previous_color(color)
        
        # Show the dialog
        response = self.colorseldlg.run()
        if response == gtk.RESPONSE_OK:
           color = colorsel.get_current_color()
        self.colorseldlg.hide()
        return color            
        
    def button_press_event(self, widget, event):
        x, y, b = event.x, event.y, event.button
        #print x, y, b
        self.adjust_scale((0,1,0,-1)[b])
        return True

    def wheel_scroll_event(self, widget, event):
        x, y, d = event.x, event.y, event.direction.value_nick
        #print x, y, d
        #self.adjust_scale((-1,+1)[d=='up'])        
        return True

def colortext(c):
  ''' Convert color to 48 bit hex string '''
  return '#%04x%04x%04x' % (c.red, c.green, c.blue)
      
def BMtoXPM(width, height, pat, bscale):
    ''' Convert bitmap data to XPM, with scaling '''    
    if bscale > 0:
        scale = 2 ** bscale
        printe(1, 'Scaling by %d' % scale)
        spat = []
        for row in pat:
            srow = []
            for i in row:
                srow += scale*[i]
            spat += scale*[srow]
        width *= scale
        height *= scale
    elif bscale < 0:
        scale = 2 ** (-bscale)
        printe(1, 'Scaling by 1/%d' % scale)
        spat = [row[::scale]for row in pat[::scale]]
        width //= scale
        height //= scale 
    else:
        spat = pat   
    
    #Create xpm bitmap data from pattern
    xpm = [''.join(['01'[i] for i in row]) for row in spat]          
    #for row in xpm: print ''.join(row)
    return width, height, xpm
    
def binpower(x):
    ''' Convert float x to an integer binary power '''
    b = 0
    if x>1.:
        while x>1.:
            x /= 2.
            b += 1
    elif x<1:        
        while x<1.:
            x *= 2.
            b -= 1 
    return b  
    
def main():
    title = 'LifeRLE'
    if len(sys.argv) <= 1:
        #No arg, use stdin
        #src = sys.stdin
        src = myline()
    elif os.path.exists(sys.argv[1]):
        #If first arg is a file, use it
        src = open(sys.argv[1], 'rb')
        title = '%s - %s' % (title, os.path.basename(sys.argv[1])) 
    else:
        #Otherwise, treat all args as input string
        src = ''.join(sys.argv[1:])
                
    printe(1, 'source = %s' % src)
    
    #Create window
    pmi = PixmapImage(title)
    
    #Decode RLE data from src
    width, height, pat = decodeRLE(src)
    
    #Set initial image scale
    s = min(MAXWIDTH/float(width), MAXHEIGHT/float(height))
    s = min(8, max(1./256, s))      
    bscale = binpower(s)                 
    printe(1, 'initial scale = 2^%d' % bscale)
    
    #Set minimum scale
    s = min(width, height)
    minbscale = 1 - binpower(s) 
    printe(1, 'minimum scale = 2^%d' % minbscale)
    
    #Store bitmap & show image
    pmi.set_bitmap(width, height, bscale, minbscale, pat)    
    pmi.show_image()
    gtk.main()

if __name__  ==  '__main__':
    main()

Re: Life file format converters and display programs

Posted: July 7th, 2009, 6:37 am
by dvgrn
PM 2Ring wrote:RLE file viewerThis Python program is a fairly basic RLE file viewer... I wrote it to make it possible to easily view RLE files in a file manager (like Konqueror, the KDE file manager). It's not very fast, but for smaller patterns it's faster than starting up Golly, and rather more convenient than dragging files from the file manager into Golly when you're scanning through a large folder looking for a particular pattern, IMHO.
For Windows my favorite file viewer is still MCell -- it has the drive-chooser control that Golly is missing, and lets you use < and > move instantly to the previous/next pattern in the current folder. The only oddity is a weird bounce effect on my system, where if you hold down < or > and MCell can't keep up with the key repeat rate, it goes blank and starts running backwards...!

Re: Life file format converters and display programs

Posted: July 13th, 2009, 9:09 am
by PM 2Ring
Online RLE viewer.

This Javascript "bookmarklet" simplifies the task of viewing small RLE strings online, using the facilities of this website. Just save it into the Links folder of your Favourites or Bookmarks, and it'll put a button in your Links bar (it can also be operated as a menu item in Favourites or Bookmarks, but I find that less convenient. It should work in all browsers; if it doesn't please let me know.

This script will work whenever you're online, you don't have to be on the conwaylife.com site to use this facility. Of course, it requires that Javascript is enabled in your browser. :)

If you're not sure how to install this, just save any old URL to the links bar, and then edit it's name to something like "Show RLE" and paste the code below into the Properties, replacing the existing URL location entry. How to do this varies with the different browsers, but generally you can edit these things by right clicking on the button or menu entry.

Note: this script should all be on a single line, since URLs shouldn't have newlines in them. For that matter, they really shouldn't have spaces either...

Code: Select all

javascript:if(window.getSelection)zz=window.getSelection();else if(document.getSelection)zz=document.getSelection();else if(document.selection)zz=document.selection.createRange().text; location.href='http://www.conwaylife.com/image.aspx?rle='+zz
Here's a glider to test it on:

Code: Select all

bo$2bo$3o!
After viewing, just press the Back button to return to the page you were reading.

If any of these instructions aren't clear enough, please let me know.

Re: Life file format converters and display programs

Posted: July 13th, 2009, 9:58 am
by dvgrn
PM 2Ring wrote:If any of these instructions aren't clear enough, please let me know.
I got this to work in Google Chrome for the sample glider RLE, but not for RLE text from GMail messages or Google Notebook entries. Apparently JavaScript can't dig a selection out of whatever weird and wonderful AJAX stuff Google is doing...?

The unclear points for me were

1) the maximum pattern size is 96x96;
2) select the RLE chunk to display, then click the "View RLE" button;
3) don't select the header line "x = ..., y = ..." or any comments, or you'll get a messed-up pattern or "too large" error.

This last is a bit unfortunate, since it means you can't simply use the "Select All" button for most of the RLE patterns that have been posted here so far. Maybe conwaylife.com's image.aspx resource can be magically adjusted to dodge that problem...?

Re: Life file format converters and display programs

Posted: July 13th, 2009, 10:15 am
by PM 2Ring
dvgrn wrote:
PM 2Ring wrote:If any of these instructions aren't clear enough, please let me know.
I got this to work in Google Chrome for the sample glider RLE, but not for RLE text from GMail messages or Google Notebook entries. Apparently JavaScript can't dig a selection out of whatever weird and wonderful AJAX stuff Google is doing...?

The unclear points for me were

1) the maximum pattern size is 96x96;
2) select the RLE chunk to display, then click the "View RLE" button;
3) don't select the header line "x = ..., y = ..." or any comments, or you'll get a messed-up pattern or "too large" error.

This last is a bit unfortunate, since it means you can't simply use the "Select All" button for most of the RLE patterns that have been posted here so far. Maybe conwaylife.com's image.aspx resource can be magically adjusted to dodge that problem...?
Thanks for the feedback, Dave. Accessing selected text is not straightforward as the different browsers do it in different ways. As it is, my script tries 3 different methods. The problem with Chrome may be an Ajax issue, or it might be fixable by using a different Javascript function to access the selected text. I know nothing about Chrome or Ajax, but I'll ask the guys overon xkcd to see if they have any suggestions.

I don't think the header lines cause a problem to Nathaniel's software. But the linebreaks may do. I shall investigate... My script could simply filter out linebreaks (and header lines, for that matter), but such inline Javascript programs should be kept short, and this one's already rather long, due to the mucking about it does to get the selected text. I prefer a "one size fits all" approach, but I suppose I could break it up into different versions for the different browsers.

Edit. From Nathaniel's reply below, I see I don't have to worry about line breaks.

I could write a CGI script in Python that takes a more versatile approach: it'd use use image.aspx for smaller patterns & convert larger patterns into PNG image files, but that'd require Nathaniel to install my Python script & a ppmtopng conversion program on his server, as well as adding some kind of button to forum pages to activate my script.

Re: Life file format converters and display programs

Posted: July 13th, 2009, 10:22 am
by Nathaniel
dvgrn wrote:Maybe conwaylife.com's image.aspx resource can be magically adjusted to dodge that problem...?
Unfortunately, this seems to be impossible because the image.aspx script takes the RLE data in from the querystring, and the querystring automatically strips out any linebreak characters. So, for example, it's absolutely impossible for it to distinguish between:

Code: Select all

x = 3, y = 32
bo$2bo$3o
and

Code: Select all

x = 3, y = 3
2bo$2bo$3o
It's a pretty contrived example, I know, but I'm not sure how to rectify it.

Edit: Also, the image script doesn't place a cap at 96x96 patterns, but rather a maximum width (or height) of 800 pixels. Thus, for larger patterns you could have it use the following URL instead: http://www.conwaylife.com/image.aspx?rl ... o$3o&ppc=2

Replace ppc=2 by ppc=X, where X is the number of pixels per cell (default is 8 ).

Re: Life file format converters and display programs

Posted: July 13th, 2009, 12:04 pm
by dvgrn
Nathaniel wrote:...the image.aspx script takes the RLE data in from the querystring, and the querystring automatically strips out any linebreak characters. So, for example, it's absolutely impossible for it to distinguish between:

Code: Select all

x = 3, y = 32
bo$2bo$3o
and

Code: Select all

x = 3, y = 3
2bo$2bo$3o
It's a pretty contrived example, I know, but I'm not sure how to rectify it.
Hmm, doesn't look contrived to me -- that is indeed a nasty little parsing problem.

But only the second example above gives you a bounding box that matches the actual pattern. So if you were willing to try all the possible partitions of the number after the "y = ", re-parsing the RLE for each partition, and pick the best match with the bounding box, you'd get the right answer most of the time. Golly is perfectly capable of exporting an oversized bounding box like "x = 3, y = 32 bo$2bo$3o!" but people would generally not post the pattern in that form.

However, unfortunately a more common header line is "x = {width}, y = {height}, rule = B3/S23" -- which has the same problem, but trial-and-error partitioning doesn't work as well. You might well end up with two or more partitions that fit the bounding box exactly, at which point the only possible trick would be a lookup table that tells you that "B3/S23" is a much more likely rule than "B3/S2".

Ugh. Probably not a big enough win to justify the extra complexity and the performance hit, I guess.

Re: Life file format converters and display programs

Posted: July 13th, 2009, 2:29 pm
by apg
With the image.aspx querystring, you don't even need to include a RLE header; "bo$2bo$3o" alone works.

Re: Life file format converters and display programs

Posted: July 13th, 2009, 2:45 pm
by Nathaniel
calcyman wrote:With the image.aspx querystring, you don't even need to include a RLE header; "bo$2bo$3o" alone works.
Yep, the point is that the script that PM 2Ring posted would be very convenient if you could just click on the "Select All" button that appears at the top of code sections on these forums and then load the image.aspx script from that. Unfortunately, almost all of the time when people use the code tags for RLE, they include the RLE header lines (the x = ... line and possibly comment lines). It works fine if you manually select the real meat of the RLE though.

Re: Life file format converters and display programs

Posted: July 13th, 2009, 11:43 pm
by Elithrion
At the expense of making the script even longer, we could maybe add something like

Code: Select all

if(zz.indexOf("x")>-1)zz=zz.slice(zz.indexOf("\n"));
Except that doesn't seem work for a reason I can't seem to figure out. The idea being that if there's an "x" in the string it reads, we should remove the first line of it. Maybe it just doesn't read line breaks in as equivalent to "\n" or something?

Re: Life file format converters and display programs

Posted: July 14th, 2009, 2:57 am
by PM 2Ring
My Javascript's a bit rusty, but IIRC it does have some Regular Expression support. I'll see what I can do with that.

In the meantime, here's a slightly optimized version of the above. As well as being more compact, it avoids use of a global Javascript variable. Use of identifiers in these inline Javascripts can be tricky, as they may clash with existing variables on a page.

I also added the pixel size option to the end, so it can easily be adjusted manually. Is there a link, Nathaniel, where I can read about these options?

Code: Select all

javascript:location.href='http://www.conwaylife.com/image.aspx?rle='+(window.getSelection?window.getSelection():document.getSelection?document.getSelection():document.selection?document.selection.createRange().text:'')+'&ppc=8'

Re: Life file format converters and display programs

Posted: July 14th, 2009, 8:29 am
by Nathaniel
Elithrion wrote:Except that doesn't seem work for a reason I can't seem to figure out.
The reason is that the getSelection method in Javascript doesn't actually return a string, just an object containing that string. Put the line zz=zz.toString() right before the code you listed and it should work fine.
PM 2Ring wrote:Is there a link, Nathaniel, where I can read about these options?
Sure is: http://www.conwaylife.com/wiki/index.ph ... ystring.29

Re: Life file format converters and display programs

Posted: July 15th, 2009, 9:36 am
by Nathaniel
Here's an updated version of the script that works on most of the examples I've seen around the forums:

Code: Select all

javascript:if(window.getSelection)zz=window.getSelection();else if(document.getSelection)zz=document.getSelection();else if(document.selection)zz=document.selection.createRange().text;zz=zz.toString();while((zz.indexOf("\n")>-1)&&((zz.indexOf("x")>-1)||(zz.indexOf("#")>-1)))zz=zz.slice(zz.indexOf("\n")+1);location.href='http://www.conwaylife.com/image.aspx?rle='+zz+'&ppc='+Math.max(Math.ceil(8-zz.length/50),1)
It uses the idea for parsing out the first few lines provided by Elithrion, and it makes a (very) rough guess at how large the pattern is and adjusts the pixels per cell accordingly. As before, all of the code should be placed on one line.

Re: Life file format converters and display programs

Posted: August 3rd, 2009, 10:08 am
by PM 2Ring
Here's an updated version of the RLE file viewer posted above. It now uses a library function to do the scaling, rather than doing it "by hand" in Python, so it's much faster. Also, it will now output the pattern to stdout in ".o" format, if verbosity is set to 2 or greater.

Code: Select all

#!/usr/bin/env python

''' Game of Life RLE format decoder 

    Convert RLE to an integer bitmap. 
    Set verbosity to 2 (or greater) to dump pattern in '.o' format to stdout
        
    Usage: python LifeRLE.py [source]
    where source may be a file name or an RLE string.
    If no arg, source is Standard Input.
    Supports readline-style line editing on Standard Input. 
'''

import sys, os.path, readline, shlex

import pygtk
pygtk.require('2.0')
import gtk

#Screen size
MAXWIDTH = 1024 - 30
MAXHEIGHT = 768 - 30

#Debugging output level. 0=no debugging
verbosity = 0 

#Debug output routine                
def printe(v, s):
    ''' Print to stderr '''
    if v <= verbosity:
        print >>sys.stderr, s
        
def decodeRLE(src):
    ''' Decode RLE to a bitmap list of lists of int bits '''
    #Set up lexer. 
    lex = shlex.shlex(src)
    #Treat numbers, 'rule' and all other single chars as tokens
    lex.wordchars = '0123456789rule'
    #Ignore comma, equals, slash and whitespace
    lex.whitespace += ',=/'
    #lex.whitespace_split = False
    lex.eof = '!'

    width = maxwidth = height = 0
    row = []
    pat = []
    rep = 1
    
    #Read & convert data
    for tt in lex:
        if not tt: 
            break
        #printe(4, 'Token: ' + repr(tt))
        
        if tt == 'x':
            width = int(lex.get_token())
            printe(1, 'width=%d' % width)
            
        elif tt == 'y':
            height = int(lex.get_token())
            printe(1, 'height=%d' % height)
               
        elif tt == 'rule':
            for i in (0,1):
                rt = lex.get_token()
                if rt == 'B':
                    birth = lex.get_token()
                    printe(1, 'birth=%s' % birth)
                elif rt == 'S':     
                    survive = lex.get_token()
                    printe(1, 'survive=%s' % survive)
                else:
                    printe(0, 'Bad rule type: %s. Aborting' % rt) 
                    return   
                
        elif tt == 'b':
            row += rep * [0]
            rep = 1
             
        elif tt == 'o':
            row += rep * [1]
            rep = 1
            
        elif tt == '$':
            pat += [row]
            w = len(row)
            maxwidth = max(maxwidth, w)
            #printe(3, 'row=%s width=%d' % (row, w))
            rep -= 1
            if rep>0:
                pat += rep * [[0]]                       
                #printe(3, 'blanks=%d' % rep)
            row = []
            rep = 1
            
        else:            
            rep = int(tt)

    #Flush last row            
    if row:
        pat += [row]                       
        w = len(row)
        maxwidth = max(maxwidth, w)
        #printe(3, 'row=%s width=%d' % (row, w))
        
    printe(1, '')
    
    #Add rows if row count < height, or change height to match if row count > height
    h = len(pat)
    printe(1, 'height: %d, Actual height: %d' % (height, h))
    if h < height:                
        pat += (height - h) * [[0]]
    else:
        height = h            

    printe(1, 'width: %d, Actual width: %d' % (width, maxwidth))
    if width < maxwidth:        
        width = maxwidth    
    
    #Pad rows to width        
    for row in pat:
        w = len(row)
        if w < width:
            row += (width - w) * [0]

    if 2 <= verbosity:             
        for row in pat: 
            print ''.join(['.o'[i] for i in row])
        
    return width, height, pat
      
class myline:
    ''' Read from Standard Input, with line editing '''
    s = ''
    def read(self, n):
        ''' Get the next n chars '''
        while n > len(self.s):
            try:
                b = raw_input()
            except EOFError:
                return ''
            self.s += b + '\n'
        a, self.s = self.s[:n], self.s[n:]
        return a

    def readline(self):
        ''' Get the next line '''
        if 0 == len(self.s):
            try:
                b = raw_input()
            except EOFError:
                return ''
            self.s += b + '\n'
        a, self.s = self.s, ''
        return a
    
class PixmapImage:
    def __init__(self, title):
        # Create the main window
        window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        # Attach delete_event signal to terminate program
        window.connect("destroy", lambda w: gtk.main_quit())
        window.connect("key_release_event", self.key_event)        
        window.add_events(gtk.gdk.KEY_RELEASE_MASK)
        window.set_title(title)
        window.set_border_width(1)
        window.show()
        self.window = window
        self.gc = window.window.new_gc()  
        
        # A DrawingArea to contain the image
        self.da = gtk.DrawingArea()        
        self.da.connect("expose_event", self.expose_event)
        self.da.show()
        
        # Create a centering Alignment object for the DrawingArea
        align = gtk.Alignment(0.5, 0.5)
        align.show()
        align.add(self.da)        

        # A Scrolled Window to hold the aligned DrawingArea
        sw = gtk.ScrolledWindow()
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        sw.add_with_viewport(align)        
        sw.connect("button_press_event", self.button_press_event)        
        
        #sw.connect("scroll_event", self.wheel_scroll_event)
        #sw.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.SCROLL_MASK)  
        sw.add_events(gtk.gdk.BUTTON_PRESS_MASK)  
        sw.show()
        window.add(sw)   
        
        #Color palette & dialog
        self.colors = ['#000000000000', '#FFFFFFFFFFFF']          
        self.colorseldlg = None
        
        #Help dialog
        self.helpdlg = None
                      
    def set_bitmap(self, width, height, bscale, minbscale, pat):
        self.width = width
        self.height = height
        self.bscale = bscale
        self.minbscale = minbscale
        self.xpm = BMtoXPM(pat)

    def expose_event(self, widget, event):
        # Copy current pixbuf to the DrawingArea
        #x, y, width, height = event.area
        widget.window.draw_pixbuf(self.gc, self.pixbuf, 0, 0, 0, 0, 
        #widget.window.draw_pixbuf(self.gc, self.pixbuf, x, y, x, y, width, height,  
            dither=gtk.gdk.RGB_DITHER_NONE)
        return False
        
    def show_image(self, reuse=False):
        if not reuse: 
            xpmhdr = ['%d %d 2 1' % (self.width, self.height),
                '0 c %s' % self.colors[0], '1 c %s' % self.colors[1]]
            # Create the pixbuf from XPM data       
            self.pixbufbase = gtk.gdk.pixbuf_new_from_xpm_data(xpmhdr + self.xpm)
            
        w, h = self.scale_pixbuf(self.pixbufbase)           
        self.da.set_size_request(w, h)
        self.da.queue_draw() 
        if w<MAXWIDTH or h<MAXHEIGHT:
            self.window.window.resize(w+30, h+30)
        else:
            self.window.window.resize(MAXWIDTH+30, MAXHEIGHT+30)
                
    def scale_pixbuf(self, pixbuf):
        scale = 2 ** self.bscale
        w, h = self.width, self.height
        w = max(4, int(scale * w + .5))
        h = max(4, int(scale * h + .5))        
        interp_type = gtk.gdk.INTERP_NEAREST # BILINEAR TILES HYPER   
        self.pixbuf = self.pixbufbase.scale_simple(w, h, interp_type)
        return w, h

    def adjust_scale(self, delta):
        if delta==0:
            b = 0
        else:
            b = self.bscale + delta
            b = max(min(b, 4), self.minbscale)            
        if b != self.bscale:
            self.bscale = b            
            self.show_image(reuse=True)               

  # Some instructions
    def help(self):
        if self.helpdlg == None:
            self.helpdlg = gtk.Dialog("Life RLE Help", self.window,
                gtk.DIALOG_NO_SEPARATOR, (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
            label = gtk.Label("""
    Game of Life RLE file viewer. 
    Created by PM 2Ring, June 2009.
 
    Mouse:   
    Left click - zoom in.
    Right click - zoom out.
    Middle click - no scaling.
    
    Keys:
    ], * - zoom in.
    [, / - zoom out.
    1, Enter, Return - no scaling.
    
    c - select foreground color
    C - select background color
    b - invert color scheme
    
    h - this help window  
    ESC, q: quit""")
            label.show()
            self.helpdlg.vbox.pack_start(label, expand=False)
        self.helpdlg.run()    
        self.helpdlg.hide()    
                
    def key_event(self, widget, event=None):
        key = event.keyval<256 and chr(event.keyval) or event.keyval
        printe(1, 'Key: %s=%s' % (key, event.keyval))
        
        if key == 'b':
            #Invert color scheme
            self.colors = self.colors[::-1]
            self.show_image() 
        elif key in (']', '*', 65450):
            #Zoom in
            self.adjust_scale(1)
        elif key  in ('[', '/', 65455):
            #Zoom out
            self.adjust_scale(-1)
        elif key in ('1', 65457, 65421, 65293):
            #Return, Enter or 1: No scaling
            self.adjust_scale(0) 
        elif key == 'h':
            self.help()
            
        elif key == 'c':
            self.change_color(1, 'Live Cell color')
        elif key == 'C':
            self.change_color(0, 'Dead Cell color')
             
        elif key == 'q' or key == 65307:
            #Escape or q to quit            
            gtk.main_quit()           
        return True
            
    def change_color(self, colornum, title=''):
        color = gtk.gdk.color_parse(self.colors[colornum])
        newcolor = self.choose_color(color, title)
        if newcolor != color:
            self.colors[colornum] = colortext(newcolor)
            printe(1, 'Color %d: %s' % (colornum, self.colors[colornum]))
            self.show_image() 
            
    def choose_color(self, color, title=''):
        # Create color selection dialog
        if self.colorseldlg == None:
            self.colorseldlg = gtk.ColorSelectionDialog('')
        self.colorseldlg.set_title(title)            

        # Get the ColorSelection widget
        colorsel = self.colorseldlg.colorsel
        colorsel.set_current_color(color)
        colorsel.set_previous_color(color)
        
        # Show the dialog
        response = self.colorseldlg.run()
        if response == gtk.RESPONSE_OK:
           color = colorsel.get_current_color()
        self.colorseldlg.hide()
        return color            
        
    def button_press_event(self, widget, event):
        x, y, b = event.x, event.y, event.button
        printe(1, 'Mouse button #%d pressed at (%d, %d)' % (b, x, y))
        self.adjust_scale((0,1,0,-1)[b])
        return True

    # Using this prevents use of wheel to pan in window
    def wheel_scroll_event(self, widget, event):
        x, y, d = event.x, event.y, event.direction.value_nick
        printe(1, 'Mouse wheel scrolled %s at (%d, %d)' % (d, x, y))
        self.adjust_scale((-1, +1)[d=='up'])        
        return True

def colortext(c):
  ''' Convert color to 48 bit hex string '''
  return '#%04x%04x%04x' % (c.red, c.green, c.blue)
      
def BMtoXPM(pat):
    ''' Convert bitmap data to XPM '''    
    xpm = [''.join(['01'[i] for i in row]) for row in pat]          
    #for row in xpm: printe(2, ''.join(row))
    return xpm
    
def binpower(x):
    ''' Convert float x to an integer binary power '''
    b = 0
    if x>1.:
        while x>1.:
            x /= 2.
            b += 1
    elif x<1:        
        while x<1.:
            x *= 2.
            b -= 1 
    return b  
    
def main():
    title = 'LifeRLE'
    if len(sys.argv) <= 1:
        #No arg, use stdin
        #src = sys.stdin
        src = myline()
    elif os.path.exists(sys.argv[1]):
        #If first arg is a file, use it
        src = open(sys.argv[1], 'rb')
        title = '%s - %s' % (title, os.path.basename(sys.argv[1])) 
    else:
        #Otherwise, treat all args as input string
        src = ''.join(sys.argv[1:])
                
    printe(1, 'source = %s' % src)
    
    #Create window
    pmi = PixmapImage(title)
    
    #Decode RLE data from src
    width, height, pat = decodeRLE(src)
    
    #Set initial image scale
    s = min(MAXWIDTH/float(width), MAXHEIGHT/float(height))
    s = min(8, max(1./256, s))      
    bscale = binpower(s)                 
    printe(1, 'initial scale = 2^%d' % bscale)
    
    #Set minimum scale
    s = min(width, height)
    minbscale = 1 - binpower(s) 
    printe(1, 'minimum scale = 2^%d' % minbscale)
    
    #Store bitmap & show image
    pmi.set_bitmap(width, height, bscale, minbscale, pat)    
    pmi.show_image()
    gtk.main()

if __name__  ==  '__main__':
    main()