Pastie now auto-senses if line-wrap is a bad or good idea. Feedback?
## mark a section (Learn more)
#!/usr/bin/env python # -*- coding: utf-8 -*- ## changelog # Original version by Alex Ross # patch to validate html, fix hex parsing, and check PIL version by micheal -- 10/16/2007 """gen_imgmap.py Usage: gen_imgmap.py [-hi] image_file csv_file out_file Options: -i, --integer-colors Interpret color values as integers (hex is default). -h, --help Show this information. Arguments: image_file – png with one color per clickable area. The areas do not have to be contiguous, and may have holes. csv_file – csv mapping colors to tag attributes. The first row must be labels. The first column must be either a hexadecimal color, or an integer color (specify -i for integer colors). The remaining columns will be attached to the area tags as attribute values (theattribute key will be the column label). out_file – file name to which the generated image map will be written. Requirements: ImageMagick ≥ 6.3.0 Python ≥ 2.4 numpy ≥ 1.0 PIL ≥ 1.1.6\ """ import sys import time import os import getopt import csv import Image from numpy import * pil_version = Image.VERSION.split('.') pil_version =[int(e) for e in pil_version] if not (len(pil_version) == 3 and pil_version[0] >= 1 and pil_version[1] >= 1 and pil_version[2] >= 6): sys.stderr.write("your PIL version is too old %s minimum 1.1.6. needed\n" % Image.VERSION) sys.exit(1) sys.stdout = sys.stderr def save(a, name): Image.fromarray(bit2rgb(a)).save(name) def bit2rgb(b): """ Convert a bitmap to an RGB img. """ b = b[:,:,newaxis]*255 return concatenate((b, b, b), 2) def pixels_of(img): for y, x in combinations_of(range(img.shape[0]), range(img.shape[1])): yield y,x def combinations_of(A,B): for a in A: for b in B: yield a,b def dist(yx1, yx2): """ Euclidean distance from `yx1` to `yx2`.""" y1, x1 = yx1 y2, x2 = yx2 return sqrt(float((x2-x1)**2 + (y2-y1)**2)) def fill(img, yx, val): """ Fill `img` from `yx` with `val`. """ img = img.copy() edge = [yx] while edge: newedge = [] for (y,x) in edge: for (s, t) in ((y, x+1), (y, x-1), (y+1, x), (y-1, x)): try: p = img[s, t] except IndexError: pass else: if p != val: img[s, t] = val newedge.append((s, t)) edge = newedge return img def count_neighbors(yx, val, img): """ Return the number of pixels adjacent to (Y,X) with value `val`. """ c = 0 for t,s in neighbors_of(yx, corners=False): if (t < 0 or t >= img.shape[0]) or (s < 0 or s >= img.shape[1]): c+=1 continue if img[t,s] == val: c+=1 return c def neighbors_of(yx, corners=True): """ Yield the coordinates of pixels that are adjacent yx. If `corners` is False, don't yield the pixels that are adjacent to the corners of `yx`. """ y,x = yx if corners: pixels = [(y-1, x-1), (y-1, x), (y-1, x+1), ( y, x-1), ( y, x+1), (y+1, x-1), (y+1, x), (y+1, x+1)] else: pixels = [(y-1, x), ( y, x+1), (y+1, x), ( y, x-1),] for yx in pixels: yield yx def shapes_from(a): """ Yield the separate contiguous shapes of a. """ a = a.copy() while True: Y, X = a.nonzero() YX = zip(Y,X) y,x = min(YX) b = fill(a, (y,x), 0) s = logical_xor(b, a) yield s a = logical_xor(s,a) if not a.any(): break def poly_from(shp): """ Return the bounding polygon of a contiguous, solid shp. `shp` should be a binary numpy array. All parts of the shape should be connected to all other parts of the shape (contiguous). There should not be a hole in any part of the shape (solid). """ if not shp.any(): return [] Y, X = shp.nonzero() YX = zip(Y,X) y,x = min(YX) wfirst = (y, x) bfirst = (y-1, x) hist = [(wfirst, bfirst)] wcurs, bcurs = wfirst, bfirst # Traverse the shape in a clockwise manner, recording all # pixels as we go. while True: wn = [yx for yx in list(neighbors_of(wcurs))+[wcurs] if yx in YX] bn = [yx for yx in list(neighbors_of(bcurs)) + [bcurs] if not yx in YX] distances = [dist(w, b) for w, b in combinations_of(wn, bn)] temp = sorted(zip(distances, list(combinations_of(wn, bn)))) next_curs = [wb for d, wb in temp if d < 2.0 and wb not in hist] if all([w in [wh for wh, bh in hist if dist(b, bh) < 2] for w,b in next_curs]) or not next_curs: break for wb in next_curs: wcurs, bcurs = wb hist.append(wb) break poly = [w for w,b in hist] return poly def area_of(yx_1, yx_2, yx_3): """ Euclidean area of the triangle `yx_1`, `yx_2`, `yx_3`. """ y1, x1 = yx_1 y2, x2 = yx_2 y3, x3 = yx_3 return 1/2. * abs(-x2*y1 + x3*y1 + x1*y2 - x3*y2 - x1*y3 + x2*y3) def simplified(poly, min_area = 0.25): """ Return a simplified version of `poly` (`poly` is a list or array of points.). Uses a “triangle-based” simplification algorithm. For consecutive points along the edge of `poly`""" # simplify polygon i = 0 while True: if i+2 >= len(poly): break j,k,m = i, i+1, i+2 if area_of(poly[j], poly[k], poly[m]) < min_area: del poly[k] i = 0 continue else: i+=1 return poly def flip_lonely_pixels(box, min_neighbors=2): """ Fill in any lonely pixels (a pixel is lonely if it has less than 2 identical neighbors). """ print " Flipping lonely pixels…" box = box.copy() while True: boxl = box.copy() for Y, X in pixels_of(box): if count_neighbors((Y,X), box[Y,X], box) < min_neighbors: box[Y,X] = not box[Y,X] if (boxl == box).all(): break return box def fill_holes(shp): """ Return `shp` with all of its holes filled in.""" e = shp.copy() for yx in [(0,0), (-1,0), (0,-1), (-1,-1)]: d = fill(shp, yx, True) e = logical_or(e, d) if logical_not(e).any(): # the shape has a hole z_order = -1 else: z_order = 1 filled = logical_or(shp, logical_not(e)) save(filled, 'filled.png') return filled, z_order def areatags_from(mask_path=None, kwds={}): """ Return a list of html area tags for the mask at `mask_path`. Any kwds is a dictionary of attributes which are added to ALL of the returned tags. I.e. if you pass {href="http://google.com"}, all of the area tag will have “href="http://google.com"”. """ img = asarray(Image.open(mask_path)) img = img[:,:,0].astype(bool) # get the smallest box containing the shape in a. if not img.any(): return [] Y, X = img.nonzero() miny, maxy = min(Y), max(Y)+1 minx, maxx = min(X), max(X)+1 pad = 3 box = zeros((maxy-miny + 2*pad, maxx - minx + 2*pad), bool) box[pad:-pad, pad:-pad] = img[miny:maxy, minx:maxx] box = flip_lonely_pixels(box) if not box.any(): return [] # find polys print " Finding shapes…", shapes = [] for i, shp in enumerate(shapes_from(box)): print i+1, shp, z_order = fill_holes(shp) shapes.append((shp, z_order)) print print " Computing bounding polygons from shapes…" polys = [] for i, (shp, z_order) in enumerate(shapes): print " %i: z_order = %i" % (i, z_order) polys.append((poly_from(shp), z_order)) # simplify polys print " Simplifying polys…" # two passes of simplification. polys = [(simplified(poly, min_area=0.8), z_order) for poly, z_order in polys] polys = [(simplified(poly[len(poly)/2:] + poly[:len(poly)/2], min_area=0.8), z_order) for poly, z_order in polys] polys = [([(miny + y - pad + 2, minx + x - pad + 1) for y,x in poly], z_order) for poly, z_order in polys] # format html tags = [] print " Formatting area tag…" for poly, z_order in polys: tag = [] tag.append('<area shape="poly" ') for key in kwds.keys(): tag.append('%s="%s" ' % (key, kwds[key])) tag.append('coords="') tag.append(", ".join("%i,%i" % (x,y) for y,x in poly)) tag.append('"></area>\n') tags.append(("".join(tag), z_order)) return tags def rgbint(color): """ Converts an integer color code into an rgb tuple. """ color = int(color) - 2**25 return (color % 2**8, color % 2**16 / 2**8, color / 2**16) def rgbhex(color): """ Convert a hexadecimal color code into a rgb tuple. """ _2hex = lambda n: int(n,16) if len(color) in [4,7]: assert color[0] == "#" color = color[1:] if len(color) == 6: s = [color[i:i+2] for i in range(0,5,2)] else: s = [color[i] for i in range(0,3)] return tuple(map(_2hex, s)) def rgbtup(rgb): r,g,b = rgb return 2**25 + b * 2**16 + g * 2**8 + r def process_csv(csv_file): """ Generate a dictionary keyed by color from a csv. """ import csv as _csv f = open(csv_file) csv = _csv.reader(f) # the first line should be the labels attrs = {} labels = csv.next() labels = [lbl.lower() for lbl in labels] assert "color" == labels[0] for row in csv: if row: color = row[0] attrs[color] = [] for x, b in enumerate(labels[1:]): attrs[color].append((b, row[x+1].strip(" \""))) return attrs def generate_masks(base_img, attrs, integer_colors=False): masks = [] if not os.path.exists("./masks"): os.mkdir("masks") for color in attrs: if integer_colors: rgb = rgbint(color) else: rgb = rgbhex(color) label, value = attrs[color][0] new_mask = "masks/%s.png" % value if not os.path.exists(new_mask): ex = 'convert %s -transparent "rgb%r" '\ '-fx "a" +matte -negate %s' % (base_img, rgb, new_mask) print ex os.system(ex) masks.append([color, new_mask]) return masks def generate_area_tags(masks, attrs): area_tags = [] for color, m in masks: print "Processing area tag(s) for %s…" % m tags = areatags_from(m, dict(attrs[color])) if not tags: continue for tag, z_order in tags: if z_order > 0: area_tags.insert(0, tag) else: area_tags.append(tag) return area_tags def timeit(func): def wrapper(*args, **kwds): begin_time = time.time() result = func(*args, **kwds) end_time = time.time() print "Total time: %f minutes." % ((end_time - begin_time) / 60) return result return wrapper @timeit def main_(base_img, attrs_csv, output_file="out.html", integer_colors=False): attrs = process_csv(attrs_csv) masks = generate_masks(base_img, attrs, integer_colors) area_tags = generate_area_tags(masks, attrs) try: html = file(output_file, "w") html.write('<html><body>\n<map name="genmap">\n') for tag in area_tags: html.write(tag) html.write("</map>\n") html.write('<img src="%s" usemap="#genmap">\n' % base_img) html.write("</body></html>\n") finally: html.close() class Usage(Exception): def __init__(self, msg): self.msg = msg def main(argv=None): if argv is None: argv = sys.argv try: try: opts, args = getopt.getopt(argv[1:], "hi", ["help", "integer-colors"]) except getopt.error, msg: raise Usage(msg) # more code, unchanged integer_colors = False for opt in opts: if opt[0] in ["-h", "--help"]: print __doc__ return 2 elif opt[0] in ["-i", "--integer-colors"]: integer_colors = True if len(args) not in [2,3] : raise Usage("Invalid arguments.") return 2 else: base_img, attrs_csv, out_file = args main_(base_img, attrs_csv, out_file, integer_colors) except Usage, err: print >>sys.stderr, err.msg print >>sys.stderr, "for help use --help" return 2 if __name__ == "__main__": sys.exit(main())
This paste will be private.
From the Design Piracy series on my blog: