bits = 2
outfile = "out.png"
per_color = False
dither = False
use_non_random_dither = False

# End of the configuration section

import PIL.Image, sys, random
import pip._vendor.progress.bar as libbar
def format_dither(d):
    d = float(int(float(dither) * 1000)) / 10
    return str(d) + "%"
def auto_dither(bits):
    return 1.0 / (bits + 1)
if len(sys.argv) >= 3:
    outfile = sys.argv[2]
if len(sys.argv) >= 4:
    bits = int(sys.argv[3])
args = [f.lower() for f in sys.argv]
if "--dither" in args:
    dither = args[args.index("--dither") + 1].replace("%","")
    if dither == "auto":
        dither = auto_dither(bits)
        print("Using dither of " + format_dither(dither))
    else:
        dither = float(dither)
if dither > 1:
    dither /= 100
per_color = "--per-color" in args
use_non_random_dither = "--non-random-dither" in args
if use_non_random_dither:
    counter_max = int(args[args.index("--non-random-dither") + 1])
    if not dither:
        dither = auto_dither(bits)
        print("Non-random dither has no effect if dither is disabled. Guessing you want "+format_dither(dither)+"% dither")
def binprecision(start, bits, length):
    end = bin(int(start))[2:]
    while len(end) < bits:
        end = '0' + end
    return end[:length]
image = PIL.Image.open(sys.argv[1])
mode = "L" # 1x8 bit unsigned integer
if per_color:
    mode = "RGB"
elif bits == 1:
    mode = "1" # 1x1 bit unsigned integer
out = PIL.Image.new(mode,image.size)
colors = [int(i*255.0/(2**bits-1)) for i in range(2**bits)]
bar = libbar.IncrementalBar(max = image.height * image.width)
bar.start()
i = 0
if use_non_random_dither:
    counter = 1
for x in range(image.width):
    for y in range(image.height):
        i += 1
        if use_non_random_dither:
            counter += 1
            counter %= counter_max + 1
        bar.index = i
        if i % 193 == 0: bar.update() # I used to use 200 but then the last two digits of the current status were always "00" which made it look like the progress bar was fake
        pos = (x,y)
        color = image.getpixel(pos)
        if type(color) == int:
            color = (color,)
        if len(color) == 4:
            color = color[:3] # Exclude alpha layer
        color = list(color)
        if not per_color:
            color = [float(sum(color))/len(color)]
        for z in range(len(color)):
            if dither:
                val = 255 * random.uniform(-dither, dither)
                if use_non_random_dither:
                    val = 255*float(counter)/counter_max # goes from 0 to 255
                    val *= dither # for dither = .1 and counter max = 4 it goes from 0 to 25.5
                    val -= 255 * dither / 2 # for those values it goes from -12.75 to 12.75
                    val = int(val)
                color[z] += val
                color[z] = min(color[z], 255)
                color[z] = max(color[z], 0)
            index = int(binprecision(color[z], 8, bits), 2)
            color[z] = colors[index]
        out.putpixel(pos, tuple(color))
out.save(outfile)
bar.update() # Otherwise it will display the last multiple of 193 out of the total value
bar.finish()
