blob: e8d9f284df6c8bcd63360ceea13f494b4d024943 [file] [log] [blame]
#!/usr/bin/env python
# priodithpng
# Ordered Dither
# test with:
# priforgepng grl | priodithpng | kitty icat
# See http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT
# archived at http://web.archive.org/web/20160727202727/http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT
import sys
# https://docs.python.org/3.5/library/bisect.html
from bisect import bisect_left
import png
def dither(
out,
input,
bitdepth=1,
):
"""Dither the input PNG `inp` into an image with a smaller bit depth
and write the result image onto `out`.
`bitdepth` specifies the bit depth of the new image.
"""
# Encoding is what happened when the PNG was made (and also what
# happens when we output the PNG). Decoding is what we do to the
# source PNG in order to process it.
# The dithering algorithm is not completely general; it
# can only do bit depth reduction, not arbitrary palette changes.
maxval = 2 ** bitdepth - 1
r = png.Reader(file=input)
_, _, rows, info = r.asDirect()
planes = info["planes"]
# :todo: make an Exception
assert planes == 1
width = info["size"][0]
sourcemaxval = 2 ** info["bitdepth"] - 1
dithered_rows = run_dither(rows, info)
ninfo = dict(info)
ninfo["bitdepth"] = bitdepth
w = png.Writer(**ninfo)
w.write(out, dithered_rows)
def run_dither(rows, info):
"""Do ordered dither."""
# dither map
M = [
[0, 2],
[3, 1],
]
dw = len(M[0])
dh = len(M)
R = dh * dw
D = 2 ** info["bitdepth"] - 1
bias = sum(sum(r) for r in M) / R
_, width = info["size"]
for j, row in enumerate(rows):
targetrow = [0] * width
for i, v in enumerate(row):
mi = i % dw
mj = j % dh
mo = (M[mj][mi] - bias ) / R
k = v / D + mo
# Select nearest output code
targetrow[i] = int(k + 0.5)
# print(targetrow, file=sys.stderr)
yield targetrow
def main(argv=None):
# https://docs.python.org/3.5/library/argparse.html
import argparse
parser = argparse.ArgumentParser()
if argv is None:
argv = sys.argv
progname, *args = argv
parser.add_argument("--bitdepth", type=int, default=1, help="bitdepth of output")
parser.add_argument(
"input", nargs="?", default="-", type=png.cli_open, metavar="PNG"
)
ns = parser.parse_args(args)
return dither(png.binary_stdout(), **vars(ns))
if __name__ == "__main__":
main()