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