#---------------------------------------------------------------------------------------- # Post-It Posterizer # Version 1.1 # # Previously named "ImageManip.py" # # This program prepares a "Post-It" banner from a GIF file. The GIF should # consist of a small number of colors for which Post-Its are available. # It reads the GIF and breaks it into "pages" which are rectangular grids of # square Post-Its. It produces the following outputs: # - a listing of the color palette for the GIF # - a file with one line per page, listing the pixels for that page as indices # in the color palette # - a file with one line per page, listing the pixels using run-length encoding # - a "composite file" with one line for page, listing the pixels for that page # first as indices in the color pallette, and again using run-length encoding. # The composite file is in CSV format # - an HTML file that shows the color table, with counts of each color used. # # The output files are suitable for use in a mail-merge application that # produces an individual set of instructions for each page. Because the number # of entries in the files using run-length encoding may differ for each # line, these files may need to be opened and saved as Excel sheets in order to # be used with Word's mail merge feature. # # For example: # - If using 3"x3" Post-Its for the banner, a standard letter # sized page (8-1/2" x 11") can be covered by 12 Post-Its. In landscape mode, # the page would be 4 pixels wide by 3 pixels tall (12" x 9"), wich some # of the surface area of the Post-Its extending beyond the page. # - If using 2"x2" Post-Its, a good page size is half a letter page # (8-1/2" wide by 5-1/2" high in landscape mode). This can be covered # by 15 Post-Its, 5 across and 3 down, measuring 10" by 6". # # For each page, it prints the indices of the pixels in column-major order. # It also prints header info (#rows, #columns, and palette colors) in both files. # The program pads the image file as needed so that each page has a full # number of Post-Its on it. # # Columns are lettered, starting at A, and rows are numbered, starting at 1. # # This program uses Pillow, a fork of the Python Imaging Library (PIL) # # JL Popyack, August 2015, February 2016, April 2016, August-September 2016 # # v 1.1: August 2017 - revised method for recognizing and refraining from printing # colors not used in the GIF image. # #---------------------------------------------------------------------------------------- from __future__ import print_function from PIL import Image import os.path #---------------------------------------------------------------------------------------- # Constants #---------------------------------------------------------------------------------------- EXPANSION_FACTOR = 6 # Number of actual pixels used to display on screen PAGE_WIDTH = 5 # Number of Post-It's per width of page PAGE_HEIGHT = 3 # Number of Post-It's per height of page #---------------------------------------------------------------------------------------- # chunk(seq, n) creates a list of n-tuples from a list # from http://bytes.com/topic/python/answers/444697-retrieve-gifs-palette-entries-using-python-imaging-library-pil #---------------------------------------------------------------------------------------- def chunk(seq, size): return [seq[i:i+size] for i in range(0, len(seq), size)] #---------------------------------------------------------------------------------------- # get image file to convert #---------------------------------------------------------------------------------------- fileToConvert = raw_input("Name of file to convert? ") #fileToConvert = r"Banner_PhillySciFest_v4.gif" # hard-coded filename #fileToConvert = r"RobotSpringboard_2016.gif" # hard-coded filename im = Image.open(fileToConvert) #---------------------------------------------------------------------------------------- #---------------------------------------------------------------------------------------- # Output some statistics about the image file #---------------------------------------------------------------------------------------- print("Image Format: ",im.format, "; Image mode: ", im.mode, "Original image size: ", im.size) #---------------------------------------------------------------------------------------- # The image needs to be a proper multiple of the basic page size specified in # PAGE_WIDTH and PAGE_HEIGHT. # If it is not, the image will be resized by adding extra rows and/or columns. # An option to crop the image to a usable size has been commented out below. #---------------------------------------------------------------------------------------- maxWidth = (im.width/PAGE_WIDTH)*PAGE_WIDTH maxHeight = (im.height/PAGE_HEIGHT)*PAGE_HEIGHT mustResize = False if maxWidth < im.width: maxWidth = maxWidth + PAGE_WIDTH mustResize = True if maxHeight < im.height: maxHeight = maxHeight + PAGE_HEIGHT mustResize = True print("\nUsable size: ",(maxWidth,maxHeight)) ###---------------------------------------------------------------------------------------- ### Crop the image to the usable size. ### This feature has been disabled, but uncomment to re-enable. ###---------------------------------------------------------------------------------------- ##im = im.crop((0,0,maxWidth,maxHeight)) ##print("After cropping, Image size: ", im.size, "; Image mode: ", im.mode) #---------------------------------------------------------------------------------------- # Expand the image to the usable size if needed. # Select an appropriate color for the additional pixels added as padding #---------------------------------------------------------------------------------------- if mustResize: # newImage = Image.new("RGB", (maxWidth,maxHeight)) #default is "black" #newImage = Image.new("RGB", (maxWidth,maxHeight), "pink") newImage = Image.new("RGB", (maxWidth,maxHeight), "white") #------------------------------------------------------------------------------------ # Padding will be added uniformly to top and bottom of image #------------------------------------------------------------------------------------ h = int((maxWidth - im.width)/2) v = int((maxHeight - im.height)/2) newImage.paste(im, (h,v,h+im.width,v+im.height)) #------------------------------------------------------------------------------------ # A new image file is created, which contains the padded pixels #------------------------------------------------------------------------------------ fileToConvert = "expanded_"+fileToConvert #uses file extension to determine type newImage.save(fileToConvert) print("After expanding, Image size: ", im.size, "; Image mode: ", im.mode) im = Image.open(fileToConvert) filepath = os.path.dirname(fileToConvert) filename = os.path.basename(fileToConvert) if filepath != "": filepath = filepath + "/" print("filepath="+filepath+", filename="+filename) #---------------------------------------------------------------------------------------- # Print some statistics #---------------------------------------------------------------------------------------- bytes = im.tobytes() print("#Bytes: ",len(bytes)) data = list(im.getdata()) print("Data length: ",len(data)) #---------------------------------------------------------------------------------------- # Display the image (actual size) #---------------------------------------------------------------------------------------- im.show() #---------------------------------------------------------------------------------------- # Display the image, replacing each pixel with a nxn square, where n=EXPANSION_FACTOR #---------------------------------------------------------------------------------------- out = im.resize((im.width*EXPANSION_FACTOR, im.height*EXPANSION_FACTOR)) out.show() out.save(filepath + "Enlarged_"+filename) #---------------------------------------------------------------------------------------- # Convert image to image definition files for use in Post-It Pandemonium exercise # Use the filename prefix as part of the output file names # Three output files are created: # - one with the uncompressed data, one line per page # - one with the compressed data, one line per page # - one with both uncompressed and compressed data, one line per page #---------------------------------------------------------------------------------------- print("Processing ", filename) filebase = filename.split('.')[0] f = open(filepath + 'Post-It_'+filebase+'_uncompressed.txt', 'w') g = open(filepath + 'Post-It_'+filebase+'_compressed.txt', 'w') fg = open(filepath + 'Post-It_'+filebase+'_composite.csv', 'w') cf = open(filepath + 'ColorMap_'+filebase+'.txt', 'w') ch = open(filepath + 'ColorMap_'+filebase+'.html', 'w') print("\n\n\n\n\n

"+filename+"

", file=ch) print("\n
" , file=ch) print("\n\n \n \n \n \n", file=ch) print(maxWidth," ",maxHeight, file=cf) #---------------------------------------------------------------------------------------- # Extract the palette and print the elements in the image definition files #---------------------------------------------------------------------------------------- palette= im.getpalette() colors = chunk(palette,3) print("Palette Length: ", len(colors)) print("\nColors:") print(len(colors), file=cf) lastColor = [-1,-1,-1] for i,c in enumerate(colors): done = True for component in c: if component != i: done = False if (c != lastColor and not done): # eliminates listing same color repeatedly at end print(i,": ",c) lastColor = c #---------------------------------------------------------------------------------------- # Output color map and pixel counts to txt and html files #---------------------------------------------------------------------------------------- count = 0 index = 0 lastColor = [-1,-1,-1] for i,c in enumerate(colors): done = True for component in c: if component != i: done = False if (c != lastColor and not done): # for unused colors, color[i] may be [0,0,0] # done = False lastColor = c #pi if not done: count = index print("",file=ch) print(count,": ", end="",file=cf) print(" \n ",file=ch) print(tempstr+" ", file=ch) print(" ", file=ch) print("",file=ch) index = index + 1 count = count + 1 print("\n\nColor Count:", file=cf) totalcount = 0 for i in range(count+1): print(i,": ",data.count(i), file=cf) totalcount = totalcount + data.count(i) cf.close() print("
IndexRGB ValuesColorCount
"+str(count)+"", end="",file=ch) tempstr = " " # remove last comma print("", file=cf) print(""+str(data.count(count))+"
\n", file=ch) print("

Total Count = " + str(totalcount) + "

\n\n", file=ch) ch.close() #---------------------------------------------------------------------------------------- # Traverse the image one page at a time, and output the pixel colors for that page, # both in compressed and uncompressed form. # A "composite" file contains both the uncompressed and compressed data for each page. #---------------------------------------------------------------------------------------- h = 0 v = 0 row = 0 col = 0 numRows = maxHeight/PAGE_HEIGHT numCols = maxWidth/PAGE_WIDTH print("Total pages: ",(numCols,numRows)," = ",numCols*numRows," pages") #---------------------------------------------------------------------------------------- # Print header line for composite file: #---------------------------------------------------------------------------------------- print("PageRow,PageColumn,",end="",file=fg) for n in range(PAGE_HEIGHT*PAGE_WIDTH): print("pixel"+str(n+1)+",",end="",file=fg) for n in range(PAGE_HEIGHT*PAGE_WIDTH): print("count"+str(n+1)+",color"+str(n+1)+",",end="",file=fg) print("",file=fg) #---------------------------------------------------------------------------------------- # Print page data, one line per page: #---------------------------------------------------------------------------------------- for col in range(numCols): for row in range(numRows): #---------------------------------------------------------------------------------------- # The pages are numbered A0, A1, ..., B0, B1, ... # where A,B,C... are columns and 1,2,3... are rows #---------------------------------------------------------------------------------------- page = [] print(chr(col+65),"\t", row+1, "\t", end="", file=f) print(chr(col+65),"\t", row+1, "\t", end="", file=g) print(chr(col+65),",", row+1, ",", end="", file=fg) #---------------------------------------------------------------------------------------- # A page's pixels are extracted and stored in the "page" list # They are also output to the uncompressed file #---------------------------------------------------------------------------------------- for v in range(row*PAGE_HEIGHT,(row+1)*PAGE_HEIGHT): for h in range(col*PAGE_WIDTH,(col+1)*PAGE_WIDTH): print(data[v*maxWidth+h],"\t", end="", file=f) print(data[v*maxWidth+h],",", end="", file=fg) page = page + [ data[v*maxWidth+h] ] print("", file=f) print("", end="", file=fg) #---------------------------------------------------------------------------------------- # The page's pixels are revisited and output to the compressed file # using run-length encoding #---------------------------------------------------------------------------------------- compressed_page = [] prev = page[0] numOutputs = 0 count = 1 for cell in page[1:]: if cell==prev: count = count + 1 else: print(count,"\t",prev,"\t", end="", file=g) print(count,",",prev,",", end="", file=fg) numOutputs = numOutputs + 2 prev = cell count = 1 print(count,"\t",prev,"\t", file=g) numOutputs = numOutputs + 2 print(count,",",prev,",", end="", file=fg) #---------------------------------------------------------------------------------------- # Fill line with appropriate number of "," so that each row has the same number of # entries. This is required by Microsoft Word's Mail Merge utility. #---------------------------------------------------------------------------------------- for i in range(PAGE_HEIGHT*PAGE_WIDTH*2-numOutputs): print(",", end="", file=fg) print(file=fg) # print(","*(PAGE_HEIGHT*PAGE_WIDTH*2-numOutputs), file=fg) f.close() g.close() fg.close() print("DONE!")