213 lines
5.3 KiB
Python
213 lines
5.3 KiB
Python
import argparse
|
|
import math
|
|
import re
|
|
import subprocess
|
|
|
|
import png
|
|
|
|
|
|
class ArgumentException(Exception):
|
|
pass
|
|
|
|
|
|
|
|
class Pixel:
|
|
GRAYSCALE = "grayscale"
|
|
COLOR = "color"
|
|
|
|
_cache = {}
|
|
|
|
def __init__(self, kind, value, alpha):
|
|
self._kind = kind
|
|
self._value = value
|
|
self._alpha = alpha
|
|
|
|
@classmethod
|
|
def grayscale(cls, v, a=255):
|
|
params = (cls.GRAYSCALE, v, a)
|
|
pixel = cls._cache.get(params)
|
|
|
|
if pixel is None:
|
|
pixel = cls(cls.GRAYSCALE, v, a)
|
|
cls._cache[params] = pixel
|
|
|
|
return pixel
|
|
|
|
@classmethod
|
|
def color(cls, r, g, b, a=255):
|
|
params = (cls.COLOR, r, g, b, a)
|
|
pixel = cls._cache.get(params)
|
|
|
|
if pixel is None:
|
|
pixel = cls(cls.COLOR, (r, g, b), a)
|
|
cls._cache[params] = pixel
|
|
|
|
return pixel
|
|
|
|
def as_grayscale(self, alpha=False):
|
|
if self._kind == self.GRAYSCALE:
|
|
values = [self._value]
|
|
elif self._kind == self.COLOR:
|
|
values = [(self._value[0] + self._value[1] + self._value[2]) // 3]
|
|
|
|
if alpha:
|
|
values.append(self._alpha)
|
|
|
|
return values
|
|
|
|
def as_rgb(self, alpha=False):
|
|
if self._kind == self.GRAYSCALE:
|
|
values = [self._value] * 3
|
|
elif self._kind == self.COLOR:
|
|
values = list(self._value)
|
|
|
|
if alpha:
|
|
values.append(self._alpha)
|
|
|
|
return values
|
|
|
|
Pixel.EMPTY = Pixel.grayscale(0, a=0)
|
|
Pixel.EMPTY_GREEN = Pixel.color(0, 255, 0)
|
|
|
|
|
|
|
|
def parse_widthes(widthstr):
|
|
widthes = []
|
|
|
|
for width in widthstr.split(","):
|
|
match = re.fullmatch(r"((\d+)\*)?(\d+)", width)
|
|
if not match:
|
|
raise ArgumentException(f"invalid width: {width!r}")
|
|
|
|
width_nr = int(match.group(3))
|
|
|
|
if match.group(2):
|
|
width_times = int(match.group(2))
|
|
else:
|
|
width_times = 1
|
|
|
|
widthes.extend(width_times * [width_nr])
|
|
|
|
return widthes
|
|
|
|
def optimal_width(total):
|
|
width = 1
|
|
while total // width > width:
|
|
width += 1
|
|
|
|
return width
|
|
|
|
|
|
|
|
def load_pixels(bytestr):
|
|
for byte in bytestr:
|
|
yield Pixel.grayscale(byte)
|
|
|
|
def arrange_by_widthes(pixels, widthes):
|
|
index = 0
|
|
pixel_amount = len(pixels)
|
|
for width in widthes:
|
|
if index < pixel_amount:
|
|
#print(f"At pixel {index: 8}")
|
|
yield pixels[index:index+width]
|
|
index += width
|
|
|
|
last_width = widthes[-1]
|
|
while index < pixel_amount:
|
|
#print(f"At pixel {index: 8}")
|
|
yield pixels[index:index+last_width]
|
|
index += last_width
|
|
|
|
def pad_rows_to_width(rows, width, pad_with=Pixel.EMPTY):
|
|
for row in rows:
|
|
if len(row) < width:
|
|
delta = width - len(row)
|
|
yield row + delta * [pad_with]
|
|
else:
|
|
yield row
|
|
|
|
def rows_as_grayscale(rows, alpha=True):
|
|
for row in rows:
|
|
combined = []
|
|
for pixel in row:
|
|
combined.extend(pixel.as_grayscale(alpha=alpha))
|
|
yield combined
|
|
|
|
def rows_as_rgb(rows, alpha=True):
|
|
for row in rows:
|
|
combined = []
|
|
for pixel in row:
|
|
combined.extend(pixel.as_rgb(alpha=alpha))
|
|
yield combined
|
|
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("infile")
|
|
parser.add_argument("outfile")
|
|
parser.add_argument("--width", "-w")
|
|
parser.add_argument("--upscale", "-u", action="store_true")
|
|
parser.add_argument("--transparent", "-t", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
# The following code should probably be put into separate methods 'n stuff,
|
|
# but it works for now and this is just an experimental script, so... I'll
|
|
# do it once I need to add more functionality.
|
|
|
|
if args.transparent:
|
|
alpha = True
|
|
greyscale = True
|
|
pad_with = Pixel.EMPTY
|
|
else:
|
|
alpha = False
|
|
greyscale = False
|
|
pad_with = Pixel.EMPTY_GREEN
|
|
|
|
if args.width is not None:
|
|
widthes = parse_widthes(args.width)
|
|
else:
|
|
widthes = None # determined once we know the amount of pixels
|
|
|
|
print(f"Reading from {args.infile!r}")
|
|
with open(args.infile, "rb") as f:
|
|
bytestr = f.read()
|
|
print(f"{len(bytestr)} bytes")
|
|
|
|
pixels = list(load_pixels(bytestr))
|
|
print(f"{len(pixels)} pixels")
|
|
|
|
if widthes is None:
|
|
widthes = [optimal_width(len(pixels))]
|
|
|
|
print("Arranging pixels into rows")
|
|
rows = list(arrange_by_widthes(pixels, widthes))
|
|
max_width = max(map(len, rows))
|
|
height = len(rows)
|
|
print(f"{height} rows, maximum width is {max_width}")
|
|
print("Padding rows to the right width")
|
|
padded_rows = list(pad_rows_to_width(rows, max_width, pad_with))
|
|
|
|
print("Converting rows to raw pixel values")
|
|
if greyscale:
|
|
value_rows = rows_as_grayscale(padded_rows, alpha=alpha)
|
|
else:
|
|
value_rows = rows_as_rgb(padded_rows, alpha=alpha)
|
|
|
|
print(f"Writing to {args.outfile!r}")
|
|
writer = png.Writer(greyscale=greyscale, alpha=alpha,
|
|
width=max_width, height=height)
|
|
with open(args.outfile, "wb") as f:
|
|
writer.write(f, value_rows)
|
|
|
|
if args.upscale:
|
|
print(f"Upscaling {args.outfile!r} with convert")
|
|
subprocess.run([
|
|
"convert", args.outfile,
|
|
"-filter", "point",
|
|
"-resize", "1000%",
|
|
args.outfile
|
|
])
|
|
|
|
if __name__ == "__main__":
|
|
main()
|