From 05770468fb9f7698d41e2069e2e30bc9bbf1bcc9 Mon Sep 17 00:00:00 2001 From: Alex Jurkiewicz Date: Tue, 5 Oct 2010 15:29:22 +1100 Subject: [PATCH 1/8] More useful error if invalid --chunklist specified. --- world.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/world.py b/world.py index 25157d1..2e6d177 100644 --- a/world.py +++ b/world.py @@ -121,6 +121,11 @@ class WorldRenderer(object): chunklist.append((base36decode(p[1]), base36decode(p[2]), path)) + if not chunklist: + logging.error("No valid chunks specified in your chunklist!") + logging.error("HINT: chunks are in your world directory and have names of the form 'c.*.*.dat'") + sys.exit(1) + # Translate to col, row coordinates _, _, _, _, chunklist = _convert_coords(chunklist) @@ -205,8 +210,6 @@ class WorldRenderer(object): p = f.split(".") all_chunks.append((base36decode(p[1]), base36decode(p[2]), os.path.join(dirpath, f))) - logging.debug((base36decode(p[1]), base36decode(p[2]), - os.path.join(dirpath, f))) if not all_chunks: logging.error("Error: No chunks found!") From a5ae7032584889362fc7ea1e342cd26190c799bb Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Sat, 9 Oct 2010 15:44:48 -0400 Subject: [PATCH 2/8] added support for ploylines and polygons on the google map Polygons and polylines are read from the new file "regions.js". Polylines (entries with "closed" set to false) are just lines drawn on the map. Polygons (entries with "closed" set to true) are closed loops that are filled in with a transparent color. --- quadtree.py | 13 +++++++++++-- template.html | 46 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/quadtree.py b/quadtree.py index 553c67f..27e3472 100644 --- a/quadtree.py +++ b/quadtree.py @@ -128,11 +128,20 @@ class QuadtreeGen(object): with open(os.path.join(self.destdir, "index.html"), 'w') as output: output.write(html) - - + # write out the default marker table with open(os.path.join(self.destdir, "markers.js"), 'w') as output: output.write("var markerData=%s" % json.dumps(self.world.POI)) + # write out the default (empty, but documented) region table + with open(os.path.join(self.destdir, "regions.js"), 'w') as output: + output.write('var regionData=[\n'); + output.write(' // {"color": "#FFAA00", "opacity": 0.5, "closed": true, "path": [\n') + output.write(' // {"x": 0, "y": 0, "z": 0},\n') + output.write(' // {"x": 0, "y": 10, "z": 0},\n') + output.write(' // {"x": 0, "y": 0, "z": 10}\n') + output.write(' // ]},\n') + output.write('];'); + # Write a blank image blank = Image.new("RGBA", (1,1)) tileDir = os.path.join(self.destdir, "tiles") diff --git a/template.html b/template.html index 4387e8a..b304077 100644 --- a/template.html +++ b/template.html @@ -8,6 +8,7 @@ #mcmap { height: 100% } + @@ -155,6 +156,48 @@ } } + var regionsInit = false; + function initRegions() { + if (regionsInit) { return; } + + regionsInit = true; + + for (i in regionData) { + var region = regionData[i]; + var converted = new google.maps.MVCArray(); + for (j in region.path) { + var point = region.path[j]; + converted.push(fromWorldToLatLng(point.x, point.y, point.z)); + } + + if (region.closed) { + new google.maps.Polygon({ + clickable: false, + geodesic: false, + map: map, + strokeColor: region.color, + strokeOpacity: region.opacity, + strokeWeight: 2, + fillColor: region.color, + fillOpacity: region.opacity * 0.25, + zIndex: i, + paths: converted + }); + } else { + new google.maps.Polyline({ + clickable: false, + geodesic: false, + map: map, + strokeColor: region.color, + strokeOpacity: region.opacity, + strokeWeight: 2, + zIndex: i, + path: converted + }); + } + } + } + function initialize() { var mapOptions = { zoom: config.defaultZoom, @@ -187,8 +230,9 @@ // We can now set the map to use the 'coordinate' map type map.setMapTypeId('mcmap'); - // initialize the markers + // initialize the markers and regions initMarkers(); + initRegions(); } From bcb423ace48612b00f659af901f4f0cd6e3107d7 Mon Sep 17 00:00:00 2001 From: Kyle Brantley Date: Sat, 9 Oct 2010 22:57:20 -0600 Subject: [PATCH 3/8] Add --optimize-img={1,2} -- performs postprocessing on images This adds basic post-processing to images. At the moment, it only performs actions for the png output type, but changes to work for jpeg will be exceptionall minimal. --- gmap.py | 8 +++++++- optimizeimages.py | 21 +++++++++++++++++++++ quadtree.py | 24 +++++++++++++++++------- 3 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 optimizeimages.py diff --git a/gmap.py b/gmap.py index aad5959..cf5a517 100755 --- a/gmap.py +++ b/gmap.py @@ -49,6 +49,7 @@ def main(): parser.add_option("--cachedir", dest="cachedir", help="Sets the directory where the Overviewer will save chunk images, which is an intermediate step before the tiles are generated. You must use the same directory each time to gain any benefit from the cache. If not set, this defaults to your world directory.") parser.add_option("--chunklist", dest="chunklist", help="A file containing, on each line, a path to a chunkfile to update. Instead of scanning the world directory for chunks, it will just use this list. Normal caching rules still apply.") parser.add_option("--imgformat", dest="imgformat", help="The image output format to use. Currently supported: png(default), jpg. NOTE: png will always be used as the intermediate image format.") + parser.add_option("--optimize-img", dest="optimizeimg", help="If using png, perform image file size optimizations on the output. Specify 1 for pngcrush, 2 for pngcrush+optipng+advdef. This may double (or more) render times, but will produce up to 30% smaller images. NOTE: requires corresponding programs in $PATH or %PATH%") parser.add_option("-q", "--quiet", dest="quiet", action="count", default=0, help="Print less output. You can specify this option multiple times.") parser.add_option("-v", "--verbose", dest="verbose", action="count", default=0, help="Print more output. You can specify this option multiple times.") @@ -98,6 +99,11 @@ def main(): else: imgformat = 'png' + if options.optimizeimg: + optimizeimg = options.optimizeimg + else: + optimizeimg = None + logging.getLogger().setLevel( logging.getLogger().level + 10*options.quiet) logging.getLogger().setLevel( @@ -111,7 +117,7 @@ def main(): w.go(options.procs) # Now generate the tiles - q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom, imgformat=imgformat) + q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom, imgformat=imgformat, optimizeimg=optimizeimg) q.go(options.procs) def delete_all(worlddir, tiledir): diff --git a/optimizeimages.py b/optimizeimages.py new file mode 100644 index 0000000..f5e87d2 --- /dev/null +++ b/optimizeimages.py @@ -0,0 +1,21 @@ +import os +import subprocess +import shlex + +def optimize_image(imgpath, imgformat, optimizeimg): + if imgformat == 'png': + if optimizeimg == "1" or optimizeimg == "2": + # we can't do an atomic replace here because windows is terrible + # so instead, we make temp files, delete the old ones, and rename + # the temp files. go windows! + subprocess.Popen(shlex.split("pngcrush " + imgpath + " " + imgpath + ".tmp"), + stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0] + os.remove(imgpath) + os.rename(imgpath+".tmp", imgpath) + + if optimizeimg == "2": + subprocess.Popen(shlex.split("optipng " + imgpath), stderr=subprocess.STDOUT, + stdout=subprocess.PIPE).communicate()[0] + subprocess.Popen(shlex.split("advdef -z4 " + imgpath), stderr=subprocess.STDOUT, + stdout=subprocess.PIPE).communicate()[0] + diff --git a/quadtree.py b/quadtree.py index 553c67f..53d34b8 100644 --- a/quadtree.py +++ b/quadtree.py @@ -24,10 +24,11 @@ import shutil import collections import json import logging +import util from PIL import Image +from optimizeimages import optimize_image -import util """ This module has routines related to generating a quadtree of tiles @@ -55,7 +56,7 @@ def catch_keyboardinterrupt(func): return newfunc class QuadtreeGen(object): - def __init__(self, worldobj, destdir, depth=None, imgformat=None): + def __init__(self, worldobj, destdir, depth=None, imgformat=None, optimizeimg=None): """Generates a quadtree from the world given into the given dest directory @@ -67,6 +68,7 @@ class QuadtreeGen(object): """ assert(imgformat) self.imgformat = imgformat + self.optimizeimg = optimizeimg if depth is None: # Determine quadtree depth (midpoint is always 0,0) @@ -235,7 +237,8 @@ class QuadtreeGen(object): # (even if tilechunks is empty, render_worldtile will delete # existing images if appropriate) yield pool.apply_async(func=render_worldtile, args= (tilechunks, - colstart, colend, rowstart, rowend, dest, self.imgformat)) + colstart, colend, rowstart, rowend, dest, self.imgformat, + self.optimizeimg)) def _apply_render_inntertile(self, pool, zoom): """Same as _apply_render_worltiles but for the inntertile routine. @@ -247,7 +250,7 @@ class QuadtreeGen(object): dest = os.path.join(self.destdir, "tiles", *(str(x) for x in path[:-1])) name = str(path[-1]) - yield pool.apply_async(func=render_innertile, args= (dest, name, self.imgformat)) + yield pool.apply_async(func=render_innertile, args= (dest, name, self.imgformat, self.optimizeimg)) def go(self, procs): """Renders all tiles""" @@ -331,7 +334,7 @@ class QuadtreeGen(object): pool.join() # Do the final one right here: - render_innertile(os.path.join(self.destdir, "tiles"), "base", self.imgformat) + render_innertile(os.path.join(self.destdir, "tiles"), "base", self.imgformat, self.optimizeimg) def _get_range_by_path(self, path): """Returns the x, y chunk coordinates of this tile""" @@ -362,7 +365,7 @@ class QuadtreeGen(object): return chunklist @catch_keyboardinterrupt -def render_innertile(dest, name, imgformat): +def render_innertile(dest, name, imgformat, optimizeimg): """ Renders a tile at os.path.join(dest, name)+".ext" by taking tiles from os.path.join(dest, name, "{0,1,2,3}.png") @@ -452,12 +455,15 @@ def render_innertile(dest, name, imgformat): img.save(imgpath, quality=95, subsampling=0) else: # png img.save(imgpath) + if optimizeimg: + optimize_image(imgpath, imgformat, optimizeimg) + with open(hashpath, "wb") as hashout: hashout.write(newhash) @catch_keyboardinterrupt -def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat): +def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat, optimizeimg): """Renders just the specified chunks into a tile and save it. Unlike usual python conventions, rowend and colend are inclusive. Additionally, the chunks around the edges are half-way cut off (so that neighboring tiles @@ -586,6 +592,10 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat # Save them tileimg.save(imgpath) + + if optimizeimg: + optimize_image(imgpath, imgformat, optimizeimg) + with open(hashpath, "wb") as hashout: hashout.write(digest) From 20e124b778650f2cf92852c9102a8b9bc8db321e Mon Sep 17 00:00:00 2001 From: Kyle Brantley Date: Sat, 9 Oct 2010 23:52:54 -0600 Subject: [PATCH 4/8] Added license to optimizeimages.py --- optimizeimages.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/optimizeimages.py b/optimizeimages.py index f5e87d2..8e34a91 100644 --- a/optimizeimages.py +++ b/optimizeimages.py @@ -1,3 +1,18 @@ +# This file is part of the Minecraft Overviewer. +# +# Minecraft Overviewer is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, or (at +# your option) any later version. +# +# Minecraft Overviewer is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with the Overviewer. If not, see . + import os import subprocess import shlex From ea94bcc9164314a0fcf1646c0dc9171d0b2a5389 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Mon, 11 Oct 2010 20:56:43 -0400 Subject: [PATCH 5/8] removed your crazy semicolons. This ain't java =) --- quadtree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quadtree.py b/quadtree.py index 27e3472..746f381 100644 --- a/quadtree.py +++ b/quadtree.py @@ -134,13 +134,13 @@ class QuadtreeGen(object): # write out the default (empty, but documented) region table with open(os.path.join(self.destdir, "regions.js"), 'w') as output: - output.write('var regionData=[\n'); + output.write('var regionData=[\n') output.write(' // {"color": "#FFAA00", "opacity": 0.5, "closed": true, "path": [\n') output.write(' // {"x": 0, "y": 0, "z": 0},\n') output.write(' // {"x": 0, "y": 10, "z": 0},\n') output.write(' // {"x": 0, "y": 0, "z": 10}\n') output.write(' // ]},\n') - output.write('];'); + output.write('];') # Write a blank image blank = Image.new("RGBA", (1,1)) From a4c0e786a1b9389471b05824cc31369491b2e47c Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Mon, 11 Oct 2010 21:03:50 -0400 Subject: [PATCH 6/8] tabs -> 4 spaces --- optimizeimages.py | 18 +++++++++--------- quadtree.py | 3 ++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/optimizeimages.py b/optimizeimages.py index 8e34a91..120b60b 100644 --- a/optimizeimages.py +++ b/optimizeimages.py @@ -19,18 +19,18 @@ import shlex def optimize_image(imgpath, imgformat, optimizeimg): if imgformat == 'png': - if optimizeimg == "1" or optimizeimg == "2": + if optimizeimg == "1" or optimizeimg == "2": # we can't do an atomic replace here because windows is terrible # so instead, we make temp files, delete the old ones, and rename # the temp files. go windows! - subprocess.Popen(shlex.split("pngcrush " + imgpath + " " + imgpath + ".tmp"), - stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0] - os.remove(imgpath) + subprocess.Popen(shlex.split("pngcrush " + imgpath + " " + imgpath + ".tmp"), + stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0] + os.remove(imgpath) os.rename(imgpath+".tmp", imgpath) - if optimizeimg == "2": - subprocess.Popen(shlex.split("optipng " + imgpath), stderr=subprocess.STDOUT, - stdout=subprocess.PIPE).communicate()[0] - subprocess.Popen(shlex.split("advdef -z4 " + imgpath), stderr=subprocess.STDOUT, - stdout=subprocess.PIPE).communicate()[0] + if optimizeimg == "2": + subprocess.Popen(shlex.split("optipng " + imgpath), stderr=subprocess.STDOUT, + stdout=subprocess.PIPE).communicate()[0] + subprocess.Popen(shlex.split("advdef -z4 " + imgpath), stderr=subprocess.STDOUT, + stdout=subprocess.PIPE).communicate()[0] diff --git a/quadtree.py b/quadtree.py index d97f3cb..8bd84ee 100644 --- a/quadtree.py +++ b/quadtree.py @@ -27,6 +27,7 @@ import logging import util from PIL import Image + from optimizeimages import optimize_image @@ -603,7 +604,7 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat tileimg.save(imgpath) if optimizeimg: - optimize_image(imgpath, imgformat, optimizeimg) + optimize_image(imgpath, imgformat, optimizeimg) with open(hashpath, "wb") as hashout: hashout.write(digest) From 33b6230babb21025a780fdf35535b3d35f08ede9 Mon Sep 17 00:00:00 2001 From: Gregory Short Date: Mon, 11 Oct 2010 23:41:59 -0500 Subject: [PATCH 7/8] Google released an update to v3 of the gmap api which enables streetview by default. This change explicitly disables it. --- template.html | 1 + 1 file changed, 1 insertion(+) diff --git a/template.html b/template.html index b304077..96ccae2 100644 --- a/template.html +++ b/template.html @@ -205,6 +205,7 @@ navigationControl: true, scaleControl: false, mapTypeControl: false, + streetViewControl: false, mapTypeId: 'mcmap' }; map = new google.maps.Map(document.getElementById("mcmap"), mapOptions); From 8b7eddf2bfa01e7520f633b169336e829b2388fc Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Tue, 12 Oct 2010 00:45:40 -0400 Subject: [PATCH 8/8] moved iteration code to its own function --- chunk.py | 187 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 96 insertions(+), 91 deletions(-) diff --git a/chunk.py b/chunk.py index 35c14c8..b1dea08 100644 --- a/chunk.py +++ b/chunk.py @@ -64,6 +64,21 @@ def get_blockdata_array(level): in a similar manner to skylight data""" return numpy.frombuffer(level['Data'], dtype=numpy.uint8).reshape((16,16,64)) +def iterate_chunkblocks(xoff,yoff): + """Iterates over the 16x16x128 blocks of a chunk in rendering order. + Yields (x,y,z,imgx,imgy) + x,y,z is the block coordinate in the chunk + imgx,imgy is the image offset in the chunk image where that block should go + """ + for x in xrange(15,-1,-1): + for y in xrange(16): + imgx = xoff + x*12 + y*12 + imgy = yoff - x*6 + y*6 + 128*12 + 16*12//2 + for z in xrange(128): + yield x,y,z,imgx,imgy + imgy -= 12 + + # This set holds blocks ids that can be seen through, for occlusion calculations transparent_blocks = set([0, 6, 8, 9, 18, 20, 37, 38, 39, 40, 44, 50, 51, 52, 53, 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 78, 79, 81, 83, 85]) @@ -260,104 +275,94 @@ class ChunkRenderer(object): if not img: img = Image.new("RGBA", (384, 1728), (38,92,255,0)) - for x in xrange(15,-1,-1): - for y in xrange(16): - imgx = xoff + x*12 + y*12 - imgy = yoff - x*6 + y*6 + 128*12 + 16*12//2 - for z in xrange(128): - try: - blockid = blocks[x,y,z] + for x,y,z,imgx,imgy in iterate_chunkblocks(xoff,yoff): + blockid = blocks[x,y,z] - # the following blocks don't have textures that can be pre-computed from the blockid - # alone. additional data is required. - # TODO torches, redstone torches, crops, ladders, stairs, - # levers, doors, buttons, and signs all need to be handled here (and in textures.py) + # the following blocks don't have textures that can be pre-computed from the blockid + # alone. additional data is required. + # TODO torches, redstone torches, crops, ladders, stairs, + # levers, doors, buttons, and signs all need to be handled here (and in textures.py) - ## minecart track, crops, ladder, doors, etc. - if blockid in textures.special_blocks: - # also handle furnaces here, since one side has a different texture than the other - ancilData = blockData_expanded[x,y,z] - try: - t = textures.specialblockmap[(blockid, ancilData)] - except KeyError: - t = None + ## minecart track, crops, ladder, doors, etc. + if blockid in textures.special_blocks: + # also handle furnaces here, since one side has a different texture than the other + ancilData = blockData_expanded[x,y,z] + try: + t = textures.specialblockmap[(blockid, ancilData)] + except KeyError: + t = None - else: - t = textures.blockmap[blockid] - if not t: - continue + else: + t = textures.blockmap[blockid] + if not t: + continue - # Check if this block is occluded - if cave and ( - x == 0 and y != 15 and z != 127 - ): - # If it's on the x face, only render if there's a - # transparent block in the y+1 direction OR the z-1 - # direction - if ( - blocks[x,y+1,z] not in transparent_blocks and - blocks[x,y,z+1] not in transparent_blocks - ): - continue - elif cave and ( - y == 15 and x != 0 and z != 127 - ): - # If it's on the facing y face, only render if there's - # a transparent block in the x-1 direction OR the z-1 - # direction - if ( - blocks[x-1,y,z] not in transparent_blocks and - blocks[x,y,z+1] not in transparent_blocks - ): - continue - elif cave and ( - y == 15 and x == 0 - ): - # If it's on the facing edge, only render if what's - # above it is transparent - if ( - blocks[x,y,z+1] not in transparent_blocks - ): - continue - elif ( - # Normal block or not cave mode, check sides for - # transparentcy or render unconditionally if it's - # on a shown face - x != 0 and y != 15 and z != 127 and - blocks[x-1,y,z] not in transparent_blocks and - blocks[x,y+1,z] not in transparent_blocks and - blocks[x,y,z+1] not in transparent_blocks - ): - # Don't render if all sides aren't transparent and - # we're not on the edge - continue + # Check if this block is occluded + if cave and ( + x == 0 and y != 15 and z != 127 + ): + # If it's on the x face, only render if there's a + # transparent block in the y+1 direction OR the z-1 + # direction + if ( + blocks[x,y+1,z] not in transparent_blocks and + blocks[x,y,z+1] not in transparent_blocks + ): + continue + elif cave and ( + y == 15 and x != 0 and z != 127 + ): + # If it's on the facing y face, only render if there's + # a transparent block in the x-1 direction OR the z-1 + # direction + if ( + blocks[x-1,y,z] not in transparent_blocks and + blocks[x,y,z+1] not in transparent_blocks + ): + continue + elif cave and ( + y == 15 and x == 0 + ): + # If it's on the facing edge, only render if what's + # above it is transparent + if ( + blocks[x,y,z+1] not in transparent_blocks + ): + continue + elif ( + # Normal block or not cave mode, check sides for + # transparentcy or render unconditionally if it's + # on a shown face + x != 0 and y != 15 and z != 127 and + blocks[x-1,y,z] not in transparent_blocks and + blocks[x,y+1,z] not in transparent_blocks and + blocks[x,y,z+1] not in transparent_blocks + ): + # Don't render if all sides aren't transparent and + # we're not on the edge + continue - # Draw the actual block on the image. For cave images, - # tint the block with a color proportional to its depth - if cave: - img.paste(Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1]) - else: - img.paste(t[0], (imgx, imgy), t[1]) + # Draw the actual block on the image. For cave images, + # tint the block with a color proportional to its depth + if cave: + img.paste(Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1]) + else: + img.paste(t[0], (imgx, imgy), t[1]) - # Draw edge lines - if blockid in (44,): # step block - increment = 6 - elif blockid in (78,): # snow - increment = 9 - else: - increment = 0 + # Draw edge lines + if blockid in (44,): # step block + increment = 6 + elif blockid in (78,): # snow + increment = 9 + else: + increment = 0 - if blockid not in transparent_blocks or blockid in (78,): #special case snow so the outline is still drawn - draw = ImageDraw.Draw(img) - if x != 15 and blocks[x+1,y,z] == 0: - draw.line(((imgx+12,imgy+increment), (imgx+22,imgy+5+increment)), fill=(0,0,0), width=1) - if y != 0 and blocks[x,y-1,z] == 0: - draw.line(((imgx,imgy+6+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1) - - - finally: - # Do this no mater how the above block exits - imgy -= 12 + if blockid not in transparent_blocks or blockid in (78,): #special case snow so the outline is still drawn + draw = ImageDraw.Draw(img) + if x != 15 and blocks[x+1,y,z] == 0: + draw.line(((imgx+12,imgy+increment), (imgx+22,imgy+5+increment)), fill=(0,0,0), width=1) + if y != 0 and blocks[x,y-1,z] == 0: + draw.line(((imgx,imgy+6+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1) return img