diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..362e53b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+oneliner
\ No newline at end of file
diff --git a/README.md b/README.md
index 56a77d5..fdf42c3 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Lid
_lo-fi image dithering_
-Lid performs fairly basic 1 bit image dithering. It converts an input image to grayscale and then dithers that image. It then optimizes the output and saves the file. The result is a reduced filesize, simple image, great for small apge sizes.
+Lid performs fairly basic 1 bit image dithering. It converts an input image to grayscale and then dithers that image. It then optimizes the output and saves the file. The result is a reduced filesize, simple image, great for small page sizes.
## Dependencies
@@ -14,8 +14,9 @@ lid [option]... [source path] [output name]
Options:
-f = output format, defaults to the recommended setting: png
--m = mode, available: o4, o9, e. default: o4.
--q = quality, defaults to 90, only affects jpeg/jpg
+-m = mode, available: o4, o9, e. default: o4.
+-q = quality (0 - 100), defaults to 90, only affects jpeg/jpg
+-t = threshold (0 - 255), defaults to image average level. only affects dither mode 't'
source path = a path to an image file you want to dither
output name = the name of the file you would like to output, do not include file extension
@@ -24,14 +25,20 @@ Lid works best outputting png files from any input. You will get the smallest fi
### Modes
+#### t
+This is the lowest quality mode. It produces the smallest file, but at the cost of most detail.
+
#### o4
-This is the lowest quality mode. It will produce the smallest file size. It is a four level ordered dither.
+This mode is a noticable difference from the stark two tone appearance of threashold mode. The appearance 'gray' areas make for a slightly more photographic image.
#### o9
-This mode produces medium quality dithering, it is still a very noticeable effect but not as harsh as o4. It is a none level ordered dither.
+This mode produces medium quality dithering, it is still a very noticeable effect but not as harsh as o4. It is a nine level ordered dither.
#### e
This is the highest quality mode. It uses an error diffusion dither. The file size will be the largest of the bunch, but should still be a good reduction from a regular full color photograph.
+#### a
+The a, or all, mode is a quick flag to say that you want an image put out in all of the modes. The mode flag value for each will be appended to the filename.
+
## License
Public domain. Use it, improve it, share it!
diff --git a/lid b/lid
index f0112d4..f474fb7 100755
--- a/lid
+++ b/lid
@@ -8,156 +8,148 @@ from PIL import Image
import sys
import os, os.path
-# Default dithering: 4 level ordered dither
-def ordered_dither_4(r):
- im = Image.open(r["path"]).convert("L")
- width, height = im.size
- new = create_image(width, height)
- newpixels = new.load()
+class Dither:
+ def __init__(self, data):
+ self.source_image = Image.open(data["path"]).convert("L")
+ self.width, self.height = self.source_image.size
+ self.pixel_count = self.width * self.height
+ self.new = self.create_image()
+ self.new_pixels = self.new.load()
+ self.output_format = data["-f"]
+ self.output_name = data["out"]
+ self.output_quality = data["-q"]
+ self.threshold = data["-t"]
- dots = [
- [64, 128],
- [192, 0]
- ]
-
- total_px_count = width * height
- counter = 1
- last_percent_val = 0
- last_hash_val = 0
- print("Dither method: ordered (4 levels)\033[?25l")
- for row in range(0, height):
- for col in range(0, width):
- dotrow = 1 if row % 2 else 0
- dotcol = 1 if col % 2 else 0
- impx = get_pixel(im, col, row)
- newpixels[col, row] = int(impx > dots[dotrow][dotcol])
- counter += 1
- new_percent_val = round(counter / total_px_count * 100)
- new_hash_val = round(counter / total_px_count * 10)
- if new_percent_val != last_percent_val or new_hash_val != last_hash_val:
- print("\r{:>3}% |{:<10}|".format(new_percent_val, "#" * new_hash_val), end="")
- last_percent_val = new_percent_val
- last_hash_val = new_hash_val
-
- new.save("{}.{}".format(r["out"], r["-f"]), r["-f"].upper(), optimize=True, quality=r["-q"])
- print("\033[20C\nFinished! ~ {}/{}.{}\033[?25h".format(os.getcwd(), r["out"], r["-f"].lower()))
-
-# 9 level ordered dither
-def ordered_dither_9(r):
- im = Image.open(r["path"]).convert("L")
- width, height = im.size
- new = create_image(width, height)
- newpixels = new.load()
-
- dots = [
- [0, 196, 84],
- [168, 140, 56],
- [112, 28, 224]
- ]
-
- total_px_count = width * height
- counter = 1
- last_percent_val = 0
- last_hash_val = 0
- print("Dither method: ordered (9 levels)\033[?25l")
- for row in range(0, height):
- for col in range(0, width):
- if not row % 3:
- dotrow = 2
- elif not row % 2:
- dotrow = 1
- else:
- dotrow = 0
-
- if not col % 3:
- dotcol = 2
- elif not col % 2:
- dotcol = 1
- else:
- dotcol = 0
- impx = get_pixel(im, col, row)
- newpixels[col, row] = int(impx > dots[dotrow][dotcol])
- counter += 1
- new_percent_val = round(counter / total_px_count * 100)
- new_hash_val = round(counter / total_px_count * 10)
- if new_percent_val != last_percent_val or new_hash_val != last_hash_val:
- print("\r{:>3}% |{:<10}|".format(new_percent_val, "#" * new_hash_val), end="")
- last_percent_val = new_percent_val
- last_hash_val = new_hash_val
-
- new.save("{}.{}".format(r["out"], r["-f"]), r["-f"].upper(), optimize=True, quality=r["-q"])
- print("\033[20C\nFinished! ~ {}/{}.{}\033[?25h".format(os.getcwd(), r["out"], r["-f"].lower()))
-
-# Alternate dither mode
-def error_dither(r):
- im = Image.open(r["path"]).convert("L")
- width, height = im.size
- new = create_image(width, height)
- newpixels = new.load()
- i = im.load()
-
- total_px_count = width * height
- counter = 1
- last_percent_val = 0
- last_hash_val = 0
- print("Dither method: error diffusion\033[?25l")
- for row in range(0, height):
- for col in range(0, width):
- newpixels[col, row] = update_error(im, i, row, col)
- counter += 1
- new_percent_val = round(counter / total_px_count * 100)
- new_hash_val = round(counter / total_px_count * 10)
- if new_percent_val != last_percent_val or new_hash_val != last_hash_val:
- print("\r{:>3}% |{:<10}|".format(new_percent_val, "#" * new_hash_val), end="")
- last_percent_val = new_percent_val
- last_hash_val = new_hash_val
-
- new.save("{}.{}".format(r["out"], r["-f"]), r["-f"].upper(), optimize=True, quality=r["-q"])
- print("\033[20C\nFinished! ~ {}/{}.{}\033[?25h".format(os.getcwd(), r["out"], r["-f"].lower()))
+ #used for printing progress
+ self.counter = 1
+ self.percent_val = 0
+ self.hash_val = 0
-def update_error(im, i, r, c):
- w, h = im.size
- current = get_pixel(im, c, r)
- if current > 128:
- res = 1
- diff = -(255 - current)
- else:
- res = 0
- diff = abs(0 - current)
- mov = [[0, 1, 0.4375],[1, 1, 0.0625],[1, 0, 0.3125],[1, -1, 0.1875]]
- for x in mov:
- if r + x[0] >= h or c + x[1] >= w or c + x[1] <= 0:
- continue
- p = get_pixel(im, c + x[1], r + x[0])
- p = round(diff * x[2] + p)
- if p < 0:
- p = 0
- elif p > 255:
- p = 255
- i[c + x[1], r + x[0]] = p
- return res
+ def save_file(self):
+ output_filename = "{}.{}".format(self.output_name, self.output_format.lower())
+ self.new.save(output_filename, self.output_format.upper(), optimize=True, quality=self.output_quality)
+ print("\033[20C\nFinished! ~ {}/{}\033[?25h".format(os.getcwd(), output_filename))
-
-def print_spinner(n):
- print("\r|{:<10}|".format("#" * n), end="")
-
-# Create a new image with the given size
-def create_image(i, j):
- image = Image.new("1", (i, j))
- return image
+ # Default dithering
+ def ordered_dither_4(self):
+ dots = [[64, 128],[192, 0]]
+ print("Dither method: ordered (4 levels)\033[?25l")
+ for row in range(0, self.height):
+ for col in range(0, self.width):
+ dotrow = 1 if row % 2 else 0
+ dotcol = 1 if col % 2 else 0
+ px = self.get_pixel(col, row)
+ self.new_pixels[col, row] = int(px > dots[dotrow][dotcol])
+ self.update_progress()
+ self.save_file()
-# Get the pixel from the given image
-def get_pixel(image, i, j):
- width, height = image.size
- if i > width or j > height:
- return None
+ def ordered_dither_9(self):
+ dots = [
+ [0, 196, 84],
+ [168, 140, 56],
+ [112, 28, 224]
+ ]
+ print("Dither method: ordered (9 levels)\033[?25l")
+ for row in range(0, self.height):
+ for col in range(0, self.width):
+ if not row % 3:
+ dotrow = 2
+ elif not row % 2:
+ dotrow = 1
+ else:
+ dotrow = 0
- # Get Pixel
- pixel = image.getpixel((i, j))
- return pixel
+ if not col % 3:
+ dotcol = 2
+ elif not col % 2:
+ dotcol = 1
+ else:
+ dotcol = 0
+ px = self.get_pixel(col, row)
+ self.new_pixels[col, row] = int(px > dots[dotrow][dotcol])
+ self.update_progress()
+ self.save_file()
+
+
+ def threshold_dither(self):
+ if not self.threshold:
+ px_list = list(self.source_image.getdata())
+ self.threshold = sum(px_list) // len(px_list)
+ print("Dither method: threshold (set to: {})\033[?25l".format(self.threshold))
+ for row in range(0, self.height):
+ for col in range(0, self.width):
+ px = self.get_pixel(col, row)
+ self.new_pixels[col, row] = int(px > self.threshold)
+ self.update_progress()
+ self.save_file()
+
+
+ def random_dither(self):
+ pass
+
+ def error_diffusion_dither(self):
+ i = self.source_image.load()
+ print("Dither method: error diffusion\033[?25l")
+ for row in range(0, self.height):
+ for col in range(0, self.width):
+ self.new_pixels[col, row] = self.update_error(i, row, col)
+ self.update_progress()
+ self.save_file()
+
+
+ # Helper for error_diffusion_dither
+ def update_error(self, i, r, c):
+ current = self.get_pixel(c, r)
+ if current > 128:
+ res = 1
+ diff = -(255 - current)
+ else:
+ res = 0
+ diff = abs(0 - current)
+ mov = [[0, 1, 0.4375],[1, 1, 0.0625],[1, 0, 0.3125],[1, -1, 0.1875]]
+ for x in mov:
+ if r + x[0] >= self.height or c + x[1] >= self.width or c + x[1] <= 0:
+ continue
+ p = self.get_pixel(c + x[1], r + x[0])
+ p = round(diff * x[2] + p)
+ if p < 0:
+ p = 0
+ elif p > 255:
+ p = 255
+ i[c + x[1], r + x[0]] = p
+ return res
+
+
+ # Create a new image with the given size
+ def create_image(self):
+ image = Image.new("1", (self.width, self.height))
+ return image
+
+
+ # Get the pixel from the given image
+ def get_pixel(self, col, row):
+ if col > self.width or row > self.height:
+ return None
+
+ # Get Pixel
+ pixel = self.source_image.getpixel((col, row))
+ return pixel
+
+
+ def update_progress(self):
+ self.counter += 1
+ new_percent_val = round(self.counter / self.pixel_count * 100)
+ new_hash_val = round(self.counter / self.pixel_count * 10)
+ if new_percent_val != self.percent_val or new_hash_val != self.hash_val:
+ print("\r{:>3}% |{:<10}|".format(new_percent_val, "#" * new_hash_val), end="")
+ self.percent_val = new_percent_val
+ self.hash_val = new_hash_val
+
+
+#---- END Dither class ----#
def display_help():
@@ -171,6 +163,7 @@ def display_help():
-f output format. defaults to: png (recommended)
-m dither mode. defaults to: o4. other options: e, o9
-q image quality. defaults to: 90. only has effect on png/jpg
+ -t threshold. default: image average tone. onls has effect in mode 't'
arguments:
infile path to a valid image file on your system
@@ -197,9 +190,10 @@ def parse_input():
argmap = {
"-f": "png", # output format
"-q": 25, # optimization quality
- "-m": "o4", # mode, defaults to ordered
+ "-m": "o4", # mode, defaults to ordered
+ "-t": None, # threshold
"path": None, # path to file
- "out": "lid_file" # output filename
+ "out": "lid_file" # output filename
}
if len(args) < 1:
print("lid error: expected image filepath, received None")
@@ -232,10 +226,20 @@ def parse_input():
elif argmap["-f"] == "jpg":
argmap["-f"] = "jpeg"
- if not argmap["-m"] in ["o4", "e", "o9"]:
+ if not argmap["-m"] in ["o4", "e", "o9", "t", "a"]:
print("lid error: invalid dither mode provided for -m. Options: o4, o9, e. Default: o4")
return None
+ try:
+ if argmap["-t"]:
+ argmap["-t"] = int(argmap["-t"])
+ if argmap["-t"] < 0 or argmap["-t"] > 255:
+ print("lid error: threshold value must be between 0 and 255")
+ return None
+ except ValueError:
+ print("lid error: invalid threshold value provided for -m. Must be integer from 0 to 255")
+ return None
+
try:
argmap["-q"] = int(argmap["-q"])
if argmap["-q"] < 1 or argmap["-q"] > 100:
@@ -254,12 +258,29 @@ def parse_input():
if __name__ == "__main__":
request = parse_input()
if request:
+ d = Dither(request)
if request["-m"] == "o4":
- ordered_dither_4(request)
+ d.ordered_dither_4()
elif request["-m"] == "o9":
- ordered_dither_9(request)
+ d.ordered_dither_9()
elif request["-m"] == "e":
- error_dither(request)
+ d.error_diffusion_dither()
+ elif request["-m"] == "t":
+ d.threshold_dither()
+ elif request["-m"] == "a":
+ name = request["out"]
+ request["out"] = name + "_o4"
+ d = Dither(request)
+ d.ordered_dither_4()
+ request["out"] = name + "_o9"
+ d2 = Dither(request)
+ d2.ordered_dither_9()
+ request["out"] = name + "_t"
+ d3 = Dither(request)
+ d3.threshold_dither()
+ request["out"] = name + "_e"
+ d4 = Dither(request)
+ d4.error_diffusion_dither()
else:
print("Unknown mode flag")
sys.exit(2)