1#!env python3
  2
  3import re
  4import os
  5import sys
  6import os.path
  7import argparse
  8
  9from PIL import Image, ImageFilter, ImageOps
 10
 11
 12def img2tex(filename, opts, outf):
 13    indent = " " * 4
 14    im = Image.open(filename).convert('L')
 15    if opts.resize:
 16        print("Resizing to {}x{}".format(opts.resize[0], opts.resize[1]))
 17        im = im.resize(opts.resize)
 18    if opts.invert:
 19        print("Inverting luminance.")
 20        im = ImageOps.invert(im)
 21    if opts.blur:
 22        print("Blurring, radius={}.".format(opts.blur))
 23        im = im.filter(ImageFilter.BoxBlur(opts.blur))
 24    if opts.rotate:
 25        if opts.rotate in (-90, 270):
 26            print("Rotating 90 degrees clockwise.".format(opts.rotate))
 27        elif opts.rotate in (90, -270):
 28            print("Rotating 90 degrees counter-clockwise.".format(opts.rotate))
 29        elif opts.rotate in (180, -180):
 30            print("Rotating 180 degrees.".format(opts.rotate))
 31        im = im.rotate(opts.rotate, expand=True)
 32    if opts.mirror_x:
 33        print("Mirroring left-to-right.")
 34        im = im.transpose(Image.FLIP_LEFT_RIGHT)
 35    if opts.mirror_y:
 36        print("Mirroring top-to-bottom.")
 37        im = im.transpose(Image.FLIP_TOP_BOTTOM)
 38    pix = im.load()
 39    width, height = im.size
 40    print("// Image {} ({}x{})".format(filename, width, height), file=outf)
 41
 42    if opts.range == "dynamic":
 43        pixmin = 255;
 44        pixmax = 0;
 45        for y in range(height):
 46            for x in range(width):
 47                pixmin = min(pixmin, pix[x,y])
 48                pixmax = max(pixmax, pix[x,y])
 49    else:
 50        pixmin = 0;
 51        pixmax = 255;
 52    print("// Original luminances: min={}, max={}".format(pixmin, pixmax), file=outf)
 53    print("// Texture heights: min={}, max={}".format(opts.minout, opts.maxout), file=outf)
 54
 55    print("{} = [".format(opts.varname), file=outf)
 56    line = indent
 57    for y in range(height):
 58        line += "[ "
 59        for x in range(width):
 60            u = (pix[x,y] - pixmin) / (pixmax - pixmin)
 61            val = u * (opts.maxout - opts.minout) + opts.minout
 62            line += "{:.3f}".format(val).rstrip('0').rstrip('.') + ", "
 63            if len(line) > 60:
 64                print(line, file=outf)
 65                line = indent * 2
 66        line += " ],"
 67        if line != indent:
 68            print(line, file=outf)
 69            line = indent
 70    print("];", file=outf)
 71    print("", file=outf)
 72
 73
 74def check_nonneg_float(value):
 75    val = float(value)
 76    if val < 0:
 77        raise argparse.ArgumentTypeError("{} is an invalid non-negative float value".format(val))
 78    return val
 79
 80
 81def main():
 82    parser = argparse.ArgumentParser(prog='img2tex')
 83    parser.add_argument('-o', '--outfile',
 84            help='Output .scad file.')
 85    parser.add_argument('-v', '--varname',
 86            help='Variable to use in .scad file.')
 87    parser.add_argument('-i', '--invert', action='store_true',
 88            help='Invert luminance values.')
 89    parser.add_argument('-r', '--resize',
 90            help='Resample image to WIDTHxHEIGHT.')
 91    parser.add_argument('-R', '--rotate', choices=(-270, -180, -90, 0, 90, 180, 270), default=0, type=int,
 92            help='Rotate output by the given number of degrees.')
 93    parser.add_argument('--mirror-x', action="store_true",
 94            help='Mirror output in the X direction.')
 95    parser.add_argument('--mirror-y', action="store_true",
 96            help='Mirror output in the Y direction.')
 97    parser.add_argument('--blur', type=check_nonneg_float, default=0,
 98            help='Perform a box blur on the output with the given radius.')
 99    parser.add_argument('--minout', type=float, default=0.0,
100            help='The value to output for the minimum luminance.')
101    parser.add_argument('--maxout', type=float, default=1.0,
102            help='The value to output for the maximum luminance.')
103    parser.add_argument('--range', choices=["dynamic", "full"], default="dynamic",
104            help='If "dynamic", the lowest to brightest luminances are scaled to the minout/maxout range.\n'
105                 'If "full", 0 to 255 luminances will be scaled to the minout/maxout range.')
106    parser.add_argument('infile', help='Input image file.')
107    opts = parser.parse_args()
108
109    non_alnum = re.compile(r'[^a-zA-Z0-9_]')
110    if not opts.varname:
111        if opts.outfile:
112            opts.varname = os.path.splitext(os.path.basename(opts.outfile))[0]
113            opts.varname = non_alnum.sub("", opts.varname)
114        else:
115            opts.varname = "image_data"
116    size_pat = re.compile(r'^([0-9][0-9]*)x([0-9][0-9]*)$')
117
118    opts.invert = bool(opts.invert)
119    
120    if opts.resize:
121        m = size_pat.match(opts.resize)
122        if not m:
123            print("Expected WIDTHxHEIGHT resize format.", file=sys.stderr)
124            sys.exit(-1)
125        opts.resize = (int(m.group(1)), int(m.group(2)))
126
127    if not opts.varname or non_alnum.search(opts.varname):
128        print("Bad variable name: {}".format(opts.varname), file=sys.stderr)
129        sys.exit(-1)
130
131    if opts.outfile:
132        with open(opts.outfile, "w") as outf:
133            img2tex(opts.infile, opts, outf)
134    else:
135        img2tex(opts.infile, opts, sys.stdout)
136
137    sys.exit(0)
138
139
140if __name__ == "__main__":
141    main()
142