Absolutely, I intend Spriter to be an open source project - only I became convinced that no one was interested in the source code
Code: Select all
# version 0.3 beta
# revision history
# 0.3.1 -> 0.3.2 Fixed a bug that prevented code from reserving white and
# black colours.
# 0.3.2 -> 0.3.3 Enabled alpha channel parsing.
# Now handles indexed PNG and alpha channels.
# Added version string
# Fixed bug that added extra pages when number of pixels
# per row/column was an exact multiple of 60.
# Legends are now organized by thread count (page-wise)
# or by DMC number (total legend)
# Now enabled multi-page legend.
_version = '0.3.3'
import png, sys, math, subprocess
# generate RGB-DMC dictionary from file obtained from http://www.xstitchtreasures.com/DMCFloss-RGBvalues.html
infile = open('dmc2rgb.csv', 'rU')
lines = infile.readlines()
infile.close()
#Floss#,Description,Red,Green,Blue,RGB code,Row
#3713,Salmon Very Light,255,226,226,FFE2E2,row 01-01
rgb2dmc = {}
for line in lines[1:]:
dmc_code, dmc_name, red, green, blue, hex, rowinfo = line.strip('\n').split(',')
rgb2dmc.update ( { (int(red), int(green), int(blue)) : (dmc_code, dmc_name) } )
# function to pick closest DMC colour based on RGB values
# use Euclidean distance for now - but human eye might be more sensitive to
# certain colour differences - check xkcd chart
def find_colour (rgb):
min_dist = 442 # maximum possible distance in 256^3 colour space
best_key = None
for key in rgb2dmc.keys():
euc_dist = math.sqrt( sum( [ (rgb[i]-key[i])**2 for i in range(3) ] ) )
if euc_dist < min_dist:
best_key = key
min_dist = euc_dist
dmc, name = rgb2dmc[best_key]
return best_key, dmc, name
# add a small feature to distinguish block from other blocks of
# similar DMC colour
def add_accent (rgb, annot, left, top, size):
half = size / 2
res = ''
# draw annotation in black or white?
if sum(rgb) > 255:
# darken
res += '%1.3f %1.3f %1.3f setrgbcolor\n' % (rgb[0]/3./255., rgb[1]/3./255., rgb[2]/3./255.)
else:
# lighten - note that values > 1 will be treated as 1.0
res += '%1.3f %1.3f %1.3f setrgbcolor\n' % (rgb[0]*3./255., rgb[1]*3./255., rgb[2]*3./255.)
res += 'newpath\n'
if annot == 1: # -
res += '%d %d moveto\n' % (left+1, top+half)
res += '%d %d lineto\n' % (left+size-1, top+half)
res += 'closepath\nstroke\n'
elif annot == 2: # |
res += '%d %d moveto\n' % (left+half, top+1)
res += '%d %d lineto\n' % (left+half, top+size-1)
res += 'closepath\nstroke\n'
elif annot == 3: # \
res += '%d %d moveto\n' % (left+1, top+1)
res += '%d %d lineto\n' % (left+size-1, top+size-1)
res += 'closepath\nstroke\n'
elif annot == 4: # /
res += '%d %d moveto\n' % (left+1, top+size-1)
res += '%d %d lineto\n' % (left+size-1, top+1)
res += 'closepath\nstroke\n'
elif annot == 5: # +
res += '%d %d moveto\n' % (left+1, top+half)
res += '%d %d lineto\n' % (left+size-1, top+half)
res += 'closepath\nstroke\n'
res += 'newpath\n'
res += '%d %d moveto\n' % (left+half, top+1)
res += '%d %d lineto\n' % (left+half, top+size-1)
res += 'closepath\nstroke\n'
elif annot == 6: # X
res += '%d %d moveto\n' % (left+2, top+2)
res += '%d %d lineto\n' % (left+size-2, top+size-2)
res += 'closepath\nstroke\n'
res += 'newpath\n'
res += '%d %d moveto\n' % (left+2, top+size-2)
res += '%d %d lineto\n' % (left+size-2, top+2)
res += 'closepath\nstroke\n'
elif annot == 7: # o filled
res += '%d %d moveto\n' % (left+half, top+half)
res += '%d %d 2 0 360 arc\n' % (left+half, top+half)
res += 'closepath\nfill\n'
elif annot == 8: # o open
res += '%d %d moveto\n' % (left+half+2, top+half)
res += '%d %d 2 0 360 arc\n' % (left+half, top+half)
res += 'closepath\nstroke\n'
elif annot == 9: # top-left filled quadrant
res += '%d %d moveto\n' % (left, top)
res += '%d %d lineto\n' % (left+half, top)
res += '%d %d lineto\n' % (left+half, top+half)
res += '%d %d lineto\n' % (left, top+half)
res += '%d %d lineto\n' % (left, top)
res += 'closepath\nfill\n'
elif annot == 9: # top-right filled quadrant
res += '%d %d moveto\n' % (left+half, top)
res += '%d %d lineto\n' % (left+size, top)
res += '%d %d lineto\n' % (left+size, top+half)
res += '%d %d lineto\n' % (left+half, top+half)
res += '%d %d lineto\n' % (left, top)
res += 'closepath\nfill\n'
elif annot == 10: # bottom-left filled quadrant
res += '%d %d moveto\n' % (left, top+half)
res += '%d %d lineto\n' % (left+half, top+half)
res += '%d %d lineto\n' % (left+half, top+size)
res += '%d %d lineto\n' % (left, top+size)
res += '%d %d lineto\n' % (left, top+half)
res += 'closepath\nfill\n'
elif annot == 11: # bottom-right filled quadrant
res += '%d %d moveto\n' % (left+half, top)
res += '%d %d lineto\n' % (left+size, top)
res += '%d %d lineto\n' % (left+size, top+half)
res += '%d %d lineto\n' % (left+half, top+half)
res += '%d %d lineto\n' % (left+half, top)
res += 'closepath\nfill\n'
elif annot == 12: # semi-circle
res += '%d %d moveto\n' % (left+half, top+half)
res += '%d %d 2 0 180 arc\n' % (left+half, top+half)
res += 'closepath\nfill\n'
elif annot == 13:
res += '%d %d moveto\n' % (left+half, top+half)
res += '%d %d 2 90 270 arc\n' % (left+half, top+half)
res += 'closepath\nfill\n'
elif annot == 14:
res += '%d %d moveto\n' % (left+half, top+half)
res += '%d %d 2 180 360 arc\n' % (left+half, top+half)
res += 'closepath\nfill\n'
elif annot == 15:
res += '%d %d moveto\n' % (left+half, top+half)
res += '%d %d 2 270 90 arc\n' % (left+half, top+half)
res += 'closepath\nfill\n'
else:
print 'ERROR: unrecognized annotation code ' + str(annot)
sys.exit()
return res
# =============================================
def main (argv=None):
if argv == None:
argv = sys.argv
if len(argv) < 2:
print 'Usage: python Spriter.py [PNG file]'
sys.exit()
#outfilename = '/tmp/'+argv[1].replace('.png', '.ps')
outfilename = argv[1].replace('.png', '.ps')
#block_size = int(sys.argv[2])
#gap_size = int(sys.argv[3])
#margin = int(sys.argv[4])
"""
for debugging
infile = open('kimpine.png')
"""
block_size = 8
gap_size = 1 # how much we skip between blocks
gap10_size = 2 # how much we skip between every 10 blocks
legend_width = 200
page_height = 612
page_width = 792
margin = 24
inset = 8
font_size = 10
# import PNG information
pr = png.Reader(argv[1])
# using asRGBA() rather than read() seems to be robust
# to alpha channel and indexed colour palettes
ncol, nrow, imap, stats = pr.asRGBA()
# generate color palette and load RGB info into Python lists
# for later processing
palette = {}
map = []
for vec in imap:
temp = []
for i in range(0, len(vec), 4):
rgb = tuple(vec[i:(i+3)].tolist())
alpha = vec[i+3]
if alpha == 0:
# full transparency, assign as white
rgb = (255, 255, 255)
temp.extend(rgb)
if palette.has_key(rgb):
palette[rgb]['total_count'] += 1
else:
palette.update ( { rgb : {'dmc':find_colour(rgb), 'total_count':1, 'page_count':0} } )
map.append(temp)
# sort palette for generating legends
sorted_palette = []
done_dmc = []
for v in palette.itervalues():
if v['dmc'] not in done_dmc:
try:
sorted_palette.append([int(v['dmc'][1]), v['dmc'], v['total_count']])
except:
sorted_palette.append([v['dmc'][1], v['dmc'], v['total_count']])
done_dmc.append(v['dmc'])
sorted_palette.sort(reverse=False)
# find nearest neighbours in colour space
threshold = 85
values = [v['dmc'] for v in palette.itervalues()]
network = [[0 for i in range(len(palette))] for j in range(len(palette))]
for node1 in range(len(values)):
for node2 in range(len(values)):
if node1 == node2:
continue
# calculate Euclidean distance in RGB colour space
if math.sqrt(sum( [ (values[node1][0][k] - values[node2][0][k])**2 for k in range(3) ] )) < threshold:
network[node1][node2] = 1
# black and white should be reserved (no annotation)
# by deleting entry in degrees list
reserved = []
for i in range(len(values)):
rgb, dmc_code, dmc_name = values[i]
if rgb in [ (0, 0, 0), (255, 255, 255) ]:
for col in range(len(network)):
network[i][col] = 0
# calculate network degree of each colour
degrees = [ sum(row) for row in network ]
# iteratively mark colours for annotation, starting with
# highest network degree
annotations = {}
rank = 1
while sum(degrees) > 0:
max_degree = max(degrees)
which_max = degrees.index(max_degree)
# mark node for annotation
this_rgb = values[which_max][0]
if not annotations.has_key(this_rgb):
annotations.update ( { this_rgb : rank } )
rank += 1
if rank > 15:
rank = 1
# update network
network[which_max] = [0 for i in range(len(network))]
for i in range(len(network)):
network[i][which_max] = 0
degrees = [ sum(row) for row in network ]
# ncol is the x (horizontal)
# nrow is the y (vertical)
# dimensions of each 8.5" x 11" page output:
# 792 x 612 pixels (landscape)
# 32 pixel margin all around (0.5 inch)
# 548 x 548 pixels square field for image + labels
# 180 x 548 pixels field for legend
# 490 x 490 (?) square field for image
# dimension of image field in number of sprite pixels
field_psize = 60
field_size = field_psize * (block_size+gap_size)
# how many pages?
xfields = int(math.ceil(ncol / float(field_psize)))
yfields = int(math.ceil(nrow / float(field_psize)))
n_pages = xfields * yfields
# start the PostScript output string
ps = ''
ps += '%%!PS\n<< /PageSize [%d %d] >> setpagedevice\n' % (page_width, page_height)
ps += '/Helvetica findfont\n'
ps += '%d scalefont\n' % font_size
ps += 'setfont\n'
# PostScript origin is at bottom-left corner of page by default
# first entry in PyPNG list is top row of image
page_num = 1
for xf in range(xfields):
# figure out the top-left corner of the current quadrant of the sprite
xorig = xf * field_psize
for yf in range(yfields):
yorig = yf * field_psize
# prepare a new page
ps += '%d %d translate\n' % (margin+inset+10, margin+inset)
# reset palette page counts
for key in palette.iterkeys():
palette[key]['page_count'] = 0
for row in range(yorig, yorig + field_psize):
try:
vec = map[row]
except IndexError:
break
except:
raise
my_top = (block_size+gap_size) * (field_psize - (row-yorig)) - gap10_size * ( (row-yorig)/10 )
if (row-yorig)%10 == 9:
ps += '0 0 0 setrgbcolor\n'
ps += '-24 %d moveto\n' % my_top
ps += '(%d) show\n' % (row+1)
# loop over columns
for col in range(xorig, xorig + field_psize):
pixel = tuple (vec[(3*col):(3*col+3)])
if not pixel:
break
rgb, dmc_code, dmc_name = palette[pixel]['dmc']
palette[pixel]['page_count'] += 1
my_left = (block_size+gap_size) * (col-xorig) + gap10_size * ( (col-xorig)/10 )
if (col-xorig)%10 == 9 and row == yorig:
ps += '0 0 0 setrgbcolor\n'
ps += '%d %d moveto\n' % (my_left+8, field_size+12)
ps += '90 rotate\n'
ps += '(%d) show\n' % (col+1)
ps += '-90 rotate\n'
ps += '%f %f %f setrgbcolor\n' % (rgb[0]/255., rgb[1]/255., rgb[2]/255.)
ps += 'newpath\n'
ps += '%d %d moveto\n' % (my_left, my_top)
ps += '%d %d lineto\n' % (my_left+block_size, my_top)
ps += '%d %d lineto\n' % (my_left+block_size, my_top+block_size)
ps += '%d %d lineto\n' % (my_left, my_top+block_size)
ps += '%d %d lineto\n' % (my_left, my_top)
# add grey outline around white block
if rgb == (255, 255, 255):
ps += 'gsave\n'
ps += '0.8 0.8 0.8 setrgbcolor\n'
ps += 'stroke\n'
ps += 'grestore\n'
ps += 'fill\n'
if annotations.has_key(rgb):
ps += add_accent (rgb, annotations[rgb], my_left, my_top, block_size)
row += 1
# add legend
ps += '%d %d translate\n' % (page_width-legend_width-margin, 0)
temp = [ (v['page_count'], v['dmc']) for v in palette.itervalues() if v['page_count'] > 0 ]
temp.sort(reverse=False)
i = 0
done_rgb = []
for page_count, dmc in temp:
rgb, dmc_code, dmc_name = dmc
# avoid duplicates
if rgb in done_rgb:
continue
else:
done_rgb.append(rgb)
my_top = i
my_left = 0
ps += '%f %f %f setrgbcolor\n' % (rgb[0]/255., rgb[1]/255., rgb[2]/255.)
ps += 'newpath\n'
ps += '%d %d moveto\n' % (my_left, my_top)
ps += '%d %d lineto\n' % (my_left+block_size, my_top)
ps += '%d %d lineto\n' % (my_left+block_size, my_top+block_size)
ps += '%d %d lineto\n' % (my_left, my_top+block_size)
ps += '%d %d lineto\n' % (my_left, my_top)
ps += 'fill\n'
if annotations.has_key(rgb):
ps += add_accent (rgb, annotations[rgb], my_left, my_top, block_size)
ps += '0 0 0 setrgbcolor\n'
#ps += '%d %d moveto\n' % (my_left + 150, my_top)
#ps += '(%d) show\n' % (v['total_count'])
#ps += '%d %d moveto\n' % (my_left + 180, my_top)
#ps += '(%d) show\n' % (v['page_count'])
ps += 'newpath\n'
ps += '%d %d moveto\n' % (block_size+gap_size, i)
ps += '( '+dmc_code+' '+dmc_name+') show\n'
i += font_size + 2
ps += '%d %d moveto\n' % (block_size+gap_size, i)
ps += '(DMC # and Name) show\n'
#ps += '%d %d moveto\n' % (150, i)
#ps += '(Total) show\n'
#ps += '%d %d moveto\n' % (180, i)
#ps += '(Page) show\n'
ps += '%d %d moveto\n' % (block_size+gap_size, i+font_size+2+6)
ps += '(Created by Spriter v'+_version+' beta) show\n'
ps += '%d %d moveto\n' % (block_size+gap_size, i+2*(font_size+2)+6)
ps += '(Page %d of %d) show\n' % (page_num, n_pages)
# output the current page
ps += 'showpage\n'
page_num += 1
# =====================================
# do final legend page
# add legend
ps += '%d %d translate\n' % (54, page_height-54)
ps += '/Helvetica findfont\n'
ps += '12 scalefont\n'
ps += 'setfont\n'
# how many columns should we draw?
# let's have 27 colours per columns
colours_per_column = 24
max_num_colours = len(palette)
num_columns = int(math.ceil(float(max_num_colours) / colours_per_column))
if num_columns > 3:
num_columns = 3
column_width = 240
for nc in range(num_columns):
ps += '%d %d moveto\n' % (nc * column_width + block_size+gap_size, 0)
ps += '(DMC # and Name) show\n'
ps += '%d %d moveto\n' % (nc * column_width + 180, 0)
ps += '(Count) show\n'
done_rgb = []
for discard, dmc, total_count in sorted_palette:
rgb, dmc_code, dmc_name = dmc
# avoid duplicates
if rgb in done_rgb:
continue
# which row and column are we in?
legend_column = (len(done_rgb)%72) / colours_per_column
legend_row = (len(done_rgb)%72) % colours_per_column
# print dmc_name + ' %d %d' % (legend_column, legend_row)
my_top = (-1) * legend_row * 16 - 16
my_left = legend_column * column_width
ps += '%f %f %f setrgbcolor\n' % (rgb[0]/255., rgb[1]/255., rgb[2]/255.)
ps += 'newpath\n'
ps += '%d %d moveto\n' % (my_left, my_top)
ps += '%d %d lineto\n' % (my_left+block_size, my_top)
ps += '%d %d lineto\n' % (my_left+block_size, my_top+block_size)
ps += '%d %d lineto\n' % (my_left, my_top+block_size)
ps += '%d %d lineto\n' % (my_left, my_top)
ps += 'fill\n'
if annotations.has_key(rgb):
ps += add_accent (rgb, annotations[rgb], my_left, my_top, block_size)
ps += '0 0 0 setrgbcolor\n'
ps += '%d %d moveto\n' % (my_left + 180, my_top)
ps += '(%d) show\n' % total_count
#ps += '%d %d moveto\n' % (my_left + 180, my_top)
#ps += '(%d) show\n' % (v['page_count'])
ps += 'newpath\n'
ps += '%d %d moveto\n' % (my_left + block_size + gap_size, my_top)
ps += '( '+dmc_code+' '+dmc_name+') show\n'
done_rgb.append(rgb)
if len(done_rgb) > 0 and len(done_rgb) % 72 == 0:
ps += '0 -420 moveto\n'
ps += '(Spriter v'+_version+' beta) show\n'
ps += '0 -436 moveto\n'
ps += '(created by ScienceDad) show\n'
ps += '0 -452 moveto\n'
ps += '(beta testing by Lord Libidan) show\n'
ps += '24 -480 moveto\n'
ps += '(Spriter is written in Python and uses the open source library pypng, http://code.google.com/p/pypng/) show\n'
ps += '24 -496 moveto\n'
ps += '(DMC to RGB colour space values were obtained from http://www.xstitchtreasures.com/DMCFloss-RGBvalues.html) show\n'
ps += 'showpage\n'
ps += '%d %d translate\n' % (54, page_height-54)
ps += '/Helvetica findfont\n'
ps += '12 scalefont\n'
ps += 'setfont\n'
for nc in range(num_columns):
ps += '%d %d moveto\n' % (nc * column_width + block_size+gap_size, 0)
ps += '(DMC # and Name) show\n'
ps += '%d %d moveto\n' % (nc * column_width + 180, 0)
ps += '(Count) show\n'
ps += '0 -420 moveto\n'
ps += '(Spriter v'+_version+' beta) show\n'
ps += '0 -436 moveto\n'
ps += '(created by ScienceDad) show\n'
ps += '0 -452 moveto\n'
ps += '(beta testing by Lord Libidan) show\n'
ps += '24 -480 moveto\n'
ps += '(Spriter is written in Python and uses the open source library pypng, http://code.google.com/p/pypng/) show\n'
ps += '24 -496 moveto\n'
ps += '(DMC to RGB colour space values were obtained from http://www.xstitchtreasures.com/DMCFloss-RGBvalues.html) show\n'
ps += 'showpage\n'
# write PostScript to file
outfile = open (outfilename, 'w')
outfile.write(ps)
outfile.close()
subprocess.call(['open', outfilename])
# ===================================
if __name__ == "__main__":
main()