#!/usr/bin/python3.14

## image-to-gcode is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by the
## Free Software Foundation; either version 2 of the License, or (at your
## option) any later version.  image-to-gcode is distributed in the hope 
## that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
## warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
## the GNU General Public License for more details.  You should have
## received a copy of the GNU General Public License along with image-to-gcode;
## if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
## Fifth Floor, Boston, MA 02110-1301 USA.
## 
## image-to-gcode.py is Copyright (C) 2005 Chris Radek
## chris@timeguy.com
## image-to-gcode.py is Copyright (C) 2006 Jeff Epler
## jepler@unpy.net

import sys, os
BASE = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), ".."))
sys.path.insert(0, os.path.join(BASE, "lib", "python"))

import gettext
gettext.install("linuxcnc", localedir=os.path.join(BASE, "share", "locale"))
def cmp(a, b):
    return (a > b) - (a < b)

try:
    from PIL import Image
except ImportError:
    import Image

import numpy.core
plus_inf = numpy.core.Inf

from rs274.author import Gcode
import rs274.options

from math import *
import operator

epsilon = 1e-5

def tobytes(img):
    if hasattr(img, 'tobytes'): return img.tobytes()
    return img.tostring()

def ball_tool(r,rad):
    s = -sqrt(rad**2-r**2)
    return s

def endmill(r,dia):
    return 0

def vee_common(angle):
    slope = tan(angle * pi / 180)
    def f(r, dia):
        return r * slope
    return f

tool_makers = [ ball_tool, endmill, vee_common(30), vee_common(45), vee_common(60)]

def make_tool_shape(f, wdia, resp):
    res = 1. / resp
    dia = int(wdia*res+.5)
    wrad = wdia/2.
    if dia < 2: dia = 2
    n = numpy.array([[plus_inf] * dia] * dia, dtype=numpy.float32)
    hdia = dia / 2.
    l = []
    for x in range(dia):
        for y in range(dia):
            r = hypot(x-hdia, y-hdia) * resp
            if r < wrad:
                z = f(r, wrad)
                l.append(z)
                n[x,y] = z
    n = n - n.min()
    return n

def amax(seq):
    res = 0
    for i in seq:
        if abs(i) > abs(res): res = i
    return res

def group_by_sign(seq, slop=sin(pi/18), key=lambda x:x):
    sign = None
    subseq = []
    for i in seq:
        ki = key(i)
        if sign is None:
            subseq.append(i)
            if ki != 0:
                sign = ki / abs(ki)
        else:
            subseq.append(i)
            if sign * ki < -slop:
                sign = ki / abs(ki)
                yield subseq
                subseq = [i]
    if subseq: yield subseq

class Convert_Scan_Alternating:
    def __init__(self):
        self.st = 0

    def __call__(self, primary, items):
        st = self.st = self.st + 1
        if st % 2: items.reverse()
        if st == 1: yield True, items
        else: yield False, items

    def reset(self):
        self.st = 0

class Convert_Scan_Increasing:
    def __call__(self, primary, items):
        yield True, items

    def reset(self):
        pass

class Convert_Scan_Decreasing:
    def __call__(self, primary, items):
        items.reverse()
        yield True, items

    def reset(self):
        pass

class Convert_Scan_Upmill:
    def __init__(self, slop = sin(pi / 18)):
        self.slop = slop

    def __call__(self, primary, items):
        for span in group_by_sign(items, self.slop, operator.itemgetter(2)):
            if amax([it[2] for it in span]) < 0:
                span.reverse()
            yield True, span

    def reset(self):
        pass

class Convert_Scan_Downmill:
    def __init__(self, slop = sin(pi / 18)):
        self.slop = slop

    def __call__(self, primary, items):
        for span in group_by_sign(items, self.slop, operator.itemgetter(2)):
            if amax([it[2] for it in span]) > 0:
                span.reverse()
            yield True, span

    def reset(self):
        pass

class Reduce_Scan_Lace:
    def __init__(self, converter, slope, keep):
        self.converter = converter
        self.slope = slope
        self.keep = keep

    def __call__(self, primary, items):
        slope = self.slope
        keep = self.keep
        if primary:
            idx = 3
            test = operator.le
        else:
            idx = 2
            test = operator.ge

        def bos(j):
            return j - j % keep

        def eos(j):
            if j % keep == 0: return j
            return j + keep - j%keep

        for i, (flag, span) in enumerate(self.converter(primary, items)):
            subspan = []
            a = None
            for i, si in enumerate(span):
                ki = si[idx]
                if a is None:
                    if test(abs(ki), slope):
                        a = b = i
                else:
                    if test(abs(ki), slope):
                        b = i
                    else:
                        if i - b < keep: continue
                        yield True, span[bos(a):eos(b+1)]
                        a = None
            if a is not None:
                yield True, span[a:]

    def reset(self):
        self.converter.reset()

unitcodes = ['G20', 'G21']
convert_makers = [ Convert_Scan_Increasing, Convert_Scan_Decreasing, Convert_Scan_Alternating, Convert_Scan_Upmill, Convert_Scan_Downmill ]

def progress(a, b):
    if "AXIS_PROGRESS_BAR" in os.environ:
        print("FILTER_PROGRESS=%d" % int(a*100./b+.5), file=sys.stderr)
        sys.stderr.flush()

class Converter:
    def __init__(self,
            image, units, tool_shape, pixelsize, pixelstep, safetyheight, \
            tolerance, feed, convert_rows, convert_cols, cols_first_flag,
            entry_cut, spindle_speed, roughing_offset, roughing_delta,
            roughing_feed):
        self.image = image
        self.units = units
        self.tool = tool_shape
        self.pixelsize = pixelsize
        self.pixelstep = pixelstep
        self.safetyheight = safetyheight
        self.tolerance = tolerance
        self.base_feed = feed
        self.convert_rows = convert_rows
        self.convert_cols = convert_cols
        self.cols_first_flag = cols_first_flag
        self.entry_cut = entry_cut
        self.spindle_speed = spindle_speed
        self.roughing_offset = roughing_offset
        self.roughing_delta = roughing_delta
        self.roughing_feed = roughing_feed

        self.cache = {}

        w, h = self.w, self.h = image.shape
        ts = self.ts = tool_shape.shape[0]

        self.h1 = h - ts
        self.w1 = w - ts

        self.tool_shape = tool_shape * self.pixelsize * ts / 2;
    
    def one_pass(self):
        g = self.g
        g.set_feed(self.feed)

        if self.convert_cols and self.cols_first_flag:
            self.g.set_plane(19)
            self.mill_cols(self.convert_cols, True)
            if self.convert_rows: g.safety()
        if self.convert_rows:
            self.g.set_plane(18)
            self.mill_rows(self.convert_rows, not self.cols_first_flag)
        if self.convert_cols and not self.cols_first_flag:
            self.g.set_plane(19)
            if self.convert_rows: g.safety()
            self.mill_cols(self.convert_cols, not self.convert_rows)
        if self.convert_cols:
            self.convert_cols.reset()
        if self.convert_rows:
            self.convert_rows.reset()
        g.safety()

    def convert(self):
        self.g = g = Gcode(safetyheight=self.safetyheight,
                           tolerance=self.tolerance,
                           spindle_speed=self.spindle_speed,
                           units=self.units)
        g.begin()
        g.continuous(self.tolerance)
        g.safety()
        if self.roughing_delta and self.roughing_offset:
            base_image = self.image
            rough = make_tool_shape(ball_tool,
                                2*self.roughing_offset, self.pixelsize)
            w, h = base_image.shape
            tw, th = rough.shape
            w1 = w + tw
            h1 = h + th
            nim1 = numpy.zeros((w1, h1), dtype=numpy.float32) + base_image.min()
            nim1[int(tw/2):int(tw/2+w), int(th/2):int(th/2+h)] = base_image
            self.image = numpy.zeros((w,h), dtype=numpy.float32)
            for j in range(0, w):
                progress(j,w)
                for i in range(0, h):
                    self.image[j,i] = (nim1[j:j+tw,i:i+th] - rough).max()
            self.feed = self.roughing_feed
            r = -self.roughing_delta
            m = self.image.min()
            self.ro = self.roughing_offset
            while r > m:
                self.rd = r
                self.one_pass()
                r = r - self.roughing_delta
            if r < m + epsilon:
                self.rd = m
                self.one_pass()
            self.image = base_image
            self.cache.clear()
        self.feed = self.base_feed
        self.ro = 0
        self.rd = self.image.min()
        self.one_pass()
        g.end()

    def get_z(self, x, y):
        try:
            return min(0, max(self.rd, self.cache[x,y]) + self.ro)
        except KeyError:
            m1 = self.image[y:y+self.ts, x:x+self.ts]
            self.cache[x,y] = d = (m1 - self.tool).max()
            return min(0, max(self.rd, d) + self.ro)
        
    def get_dz_dy(self, x, y):
        y1 = max(0, y-1)
        y2 = min(self.image.shape[0]-1, y+1)
        dy = self.pixelsize * (y2-y1)
        return (self.get_z(x, y2) - self.get_z(x, y1)) / dy
        
    def get_dz_dx(self, x, y):
        x1 = max(0, x-1)
        x2 = min(self.image.shape[1]-1, x+1)
        dx = self.pixelsize * (x2-x1)
        return (self.get_z(x2, y) - self.get_z(x1, y)) / dx

    def mill_rows(self, convert_scan, primary):
        w1 = self.w1; h1 = self.h1;
        pixelsize = self.pixelsize; pixelstep = self.pixelstep
        jrange = list(range(0, w1, pixelstep))
        if w1-1 not in jrange: jrange.append(w1-1)
        irange = list(range(h1))

        for j in jrange:
            progress(jrange.index(j), len(jrange))
            y = (w1-j) * pixelsize
            scan = []
            for i in irange:
                x = i * pixelsize
                milldata = (i, (x, y, self.get_z(i, j)),
                    self.get_dz_dx(i, j), self.get_dz_dy(i, j))
                scan.append(milldata)
            for flag, points in convert_scan(primary, scan):
                if flag:
                    self.entry_cut(self, points[0][0], j, points)
                for p in points:
                    self.g.cut(*p[1])
            self.g.flush()

    def mill_cols(self, convert_scan, primary):
        w1 = self.w1; h1 = self.h1;
        pixelsize = self.pixelsize; pixelstep = self.pixelstep
        jrange = list(range(0, h1, pixelstep))
        irange = list(range(w1))
        if h1-1 not in jrange: jrange.append(h1-1)
        jrange.reverse()

        for j in jrange:
            progress(jrange.index(j), len(jrange))
            x = j * pixelsize
            scan = []
            for i in irange:
                y = (w1-i) * pixelsize
                milldata = (i, (x, y, self.get_z(j, i)),
                    self.get_dz_dy(j, i), self.get_dz_dx(j, i))
                scan.append(milldata)
            for flag, points in convert_scan(primary, scan):
                if flag:
                    self.entry_cut(self, j, points[0][0], points)
                for p in points:
                    self.g.cut(*p[1])
            self.g.flush()

def convert(*args, **kw):
    return Converter(*args, **kw).convert()

class SimpleEntryCut:
    def __init__(self, feed):
        self.feed = feed

    def __call__(self, conv, i0, j0, points):
        p = points[0][1]
        if self.feed:
            conv.g.set_feed(self.feed)
        conv.g.safety()
        conv.g.rapid(p[0], p[1])
        if self.feed:
            conv.g.set_feed(conv.feed)

def circ(r,b): 
    """\
Calculate the portion of the arc to do so that none is above the
safety height (that's just silly)"""

    z = r**2 - (r-b)**2
    if z < 0: z = 0
    return z**.5

class ArcEntryCut:
    def __init__(self, feed, max_radius):
        self.feed = feed
        self.max_radius = max_radius

    def __call__(self, conv, i0, j0, points):
        if len(points) < 2:
            p = points[0][1]
            if self.feed:
                conv.g.set_feed(self.feed)
            conv.g.safety()
            conv.g.rapid(p[0], p[1])
            if self.feed:
                conv.g.set_feed(conv.feed)
            return

        p1 = points[0][1]
        p2 = points[1][1]
        z0 = p1[2]

        lim = int(ceil(self.max_radius / conv.pixelsize))
        r = list(range(1, lim))

        if self.feed:
            conv.g.set_feed(self.feed)
        conv.g.safety()

        x, y, z = p1

        pixelsize = conv.pixelsize
        
        cx = cmp(p1[0], p2[0])
        cy = cmp(p1[1], p2[1])

        radius = self.max_radius

        if cx != 0:
            h1 = conv.h1
            for di in r:
                dx = di * pixelsize
                i = i0 + cx * di
                if i < 0 or i >= h1: break
                z1 = conv.get_z(i, j0)
                dz = (z1 - z0)
                if dz <= 0: continue
                if dz > dx:
                    conv.g.write("(case 1)")
                    radius = dx
                    break
                rad1 = (dx * dx / dz + dz) / 2
                if rad1 < radius:
                    radius = rad1
                if dx > radius:
                    break

            z1 = min(p1[2] + radius, conv.safetyheight)
            x1 = p1[0] + cx * circ(radius, z1 - p1[2])
            conv.g.rapid(x1, p1[1])
            conv.g.cut(z=z1)

            conv.g.flush(); conv.g.lastgcode = None
            if cx > 0:
                conv.g.write("G3 X%f Z%f R%f" % (p1[0], p1[2], radius))
            else:
                conv.g.write("G2 X%f Z%f R%f" % (p1[0], p1[2], radius))
            conv.g.lastx = p1[0]
            conv.g.lasty = p1[1]
            conv.g.lastz = p1[2]
        else:
            w1 = conv.w1
            for dj in r:
                dy = dj * pixelsize
                j = j0 - cy * dj
                if j < 0 or j >= w1: break
                z1 = conv.get_z(i0, j)
                dz = (z1 - z0)
                if dz <= 0: continue
                if dz > dy:
                    radius = dy
                    break
                rad1 = (dy * dy / dz + dz) / 2
                if rad1 < radius: radius = rad1
                if dy > radius: break

            z1 = min(p1[2] + radius, conv.safetyheight)
            y1 = p1[1] + cy * circ(radius, z1 - p1[2])
            conv.g.rapid(p1[0], y1)
            conv.g.cut(z=z1)

            conv.g.flush(); conv.g.lastgcode = None
            if cy > 0:
                conv.g.write("G2 Y%f Z%f R%f" % (p1[1], p1[2], radius))
            else:
                conv.g.write("G3 Y%f Z%f R%f" % (p1[1], p1[2], radius))
            conv.g.lastx = p1[0]
            conv.g.lasty = p1[1]
            conv.g.lastz = p1[2]
        if self.feed:
            conv.g.set_feed(conv.feed)

def ui(im, nim, im_name):
    import tkinter

    try:
        from PIL import ImageTk
    except ImportError:    
        import ImageTk

    import pickle
    import nf

    app = tkinter.Tk()
    rs274.options.install(app)
    app.tk.call("source", os.path.join(BASE, "share", "axis", "tcl", "combobox.tcl"))

    name = os.path.basename(im_name)
    app.wm_title(_("%s: Image to G-code") % name)
    app.wm_iconname(_("Image to G-code"))
    w, h = im.size
    r1 = w / 300.
    r2 = h / 300.
    nw = int(w / max(r1, r2))
    nh = int(h / max(r1, r2))

    ui_image = im.resize((nw,nh), Image.LANCZOS)
    ui_image = ImageTk.PhotoImage(ui_image, master = app)
    i = tkinter.Label(app, image=ui_image, compound="top",
        text=_("Image size: %(w)d x %(h)d pixels\n"
                "Minimum pixel value: %(min)d\nMaximum pixel value: %(max)d")
            % {'w': im.size[0], 'h': im.size[1], 'min': nim.min(), 'max': nim.max()},
        justify="left")
    f = tkinter.Frame(app)
    g = tkinter.Frame(app)
    b = tkinter.Frame(app)
    i.grid(row=0, column=0, sticky="nw")
    f.grid(row=0, column=1, sticky="nw")
    b.grid(row=1, column=0, columnspan=2, sticky="ne")

    def filter_nonint(event):
        if event.keysym in ("Return", "Tab", "ISO_Left_Tab", "BackSpace"):
            return
        if event.char == "": return
        if event.char in "0123456789": return
        return "break"

    def filter_nonfloat(event):
        if event.keysym in ("Return", "Tab", "ISO_Left_Tab", "BackSpace"):
            return
        if event.char == "": return
        if event.char in "0123456789.": return
        return "break"
        
    validate_float    = r"expr {![regexp {^-?([0-9]+(\.[0-9]*)?|\.[0-9]+|)$} %P]}"
    validate_int      = "expr {![regexp {^-?([0-9]+|)$} %P]}"
    validate_posfloat = r"expr {![regexp {^?([0-9]+(\.[0-9]*)?|\.[0-9]+|)$} %P]}"
    validate_posint   = "expr {![regexp {^([0-9]+|)$} %P]}"
    def floatentry(f, v):
        var = tkinter.DoubleVar(f)
        var.set(v)
        w = tkinter.Entry(f, textvariable=var, validatecommand=validate_float, validate="key", width=10)
        return w, var

    def intentry(f, v):
        var = tkinter.IntVar(f)
        var.set(v)
        w = tkinter.Entry(f, textvariable=var, validatecommand=validate_int, validate="key", width=10)
        return w, var

    def checkbutton(k, v):
        var = tkinter.BooleanVar(f)
        var.set(v)
        g = tkinter.Frame(f)
        w = tkinter.Checkbutton(g, variable=var, text=_("Yes"))
        w.pack(side="left")
        return g, var 

    def intscale(k, v, min=1, max = 100):
        var = tkinter.IntVar(f)
        var.set(v)
        g = tkinter.Frame(f, borderwidth=0)
        w = tkinter.Scale(g, orient="h", variable=var, from_=min, to=max, showvalue=False)
        l = tkinter.Label(g, textvariable=var, width=3)
        l.pack(side="left")
        w.pack(side="left", fill="x", expand=1)
        return g, var

    def _optionmenu(k, v, *options):
        options = list(options)
        def trace(*args):
            try:
                var.set(options.index(svar.get()))
            except ValueError:
                pass

        try:
            opt = options[v]
        except (TypeError, IndexError):
            v = 0
            opt = options[0]

        var = tkinter.IntVar(f)    
        var.set(v)
        svar = tkinter.StringVar(f)
        svar.set(options[v])
        svar.trace("w", trace)
        wp = f._w.rstrip(".") + ".c" + svar._name
        f.tk.call("combobox::combobox", wp, "-editable", 0, "-width",
                max(len(opt) for opt in options)+3, "-textvariable", svar._name,
                "-background", "white")
        f.tk.call(wp, "list", "insert", "end", *options)
        w = nf.makewidget(f, tkinter.Widget, wp)
        return w, var

    def optionmenu(*options): return lambda f, v: _optionmenu(f, v, *options)

    rc = os.path.expanduser("~/.image2gcoderc")
    constructors = [
        ("units", optionmenu(_("G20 (in)"), _("G21 (mm)"))),
        ("invert", checkbutton),
        ("normalize", checkbutton),
        ("expand", optionmenu(_("None"), _("White"), _("Black"))),
        ("tolerance", floatentry),
        ("pixel_size", floatentry),
        ("feed_rate", floatentry),
        ("plunge_feed_rate", floatentry),
        ("spindle_speed", floatentry),
        ("pattern", optionmenu(_("Rows"), _("Columns"), _("Rows then Columns"), _("Columns then Rows"))),
        ("converter", optionmenu(_("Positive"), _("Negative"), _("Alternating"), _("Up Milling"), _("Down Milling"))),
        ("depth", floatentry),
        ("pixelstep", intscale),
        ("tool_diameter", floatentry),
        ("safety_height", floatentry),
        ("tool_type", optionmenu(_("Ball End"), _("Flat End"), _("30 Degree"), _("45 Degree"), _("60 Degree"))),
        ("bounded", optionmenu(_("None"), _("Secondary"), _("Full"))),
        ("contact_angle", floatentry),
        ("roughing_offset", floatentry),
        ("roughing_depth", floatentry),
    ]

    defaults = dict(
        invert = False,
        normalize = False,
        expand = 0,
        pixel_size = .006,
        depth = 0.25,
        pixelstep = 8,
        tool_diameter = 1/16.,
        safety_height = .012,
        tool_type = 0,
        tolerance = .001,
        feed_rate = 12,
        plunge_feed_rate = 12,
        units = 0,
        pattern = 0,
        converter = 0,
        bounded = 0,
        contact_angle = 45,
        spindle_speed = 1000,
        roughing_offset = .1,
        roughing_depth = .25,
    )

    texts = dict(
        invert=_("Invert Image"),
        normalize=_("Normalize Image"),
        expand=_("Extend Image Border"),
        pixel_size=_("Pixel Size (Units)"),
        depth=_("Depth (units)"),
        tolerance=_("Tolerance (units)"),
        pixelstep=_("Stepover (pixels)"),
        tool_diameter=_("Tool Diameter (units)"),
        tool_type=_("Tool Type"),
        feed_rate=_("Feed Rate (units per minute)"),
        plunge_feed_rate=_("Plunge Feed Rate (units per minute)"),
        units=_("Units"),
        safety_height=_("Safety Height (units)"),
        pattern=_("Scan Pattern"),
        converter=_("Scan Direction"),
        bounded=_("Lace Bounding"),
        contact_angle=_("Contact Angle (degrees)"),
        spindle_speed=_("Spindle Speed (RPM)"),
        roughing_offset=_("Roughing offset (units, 0=no roughing)"),
        roughing_depth=_("Roughing depth per pass (units)"),
    )

    try:
        defaults.update(pickle.load(open(rc, "rb")))
    except (IOError, pickle.PickleError): pass

    vars = {}
    widgets = {}
    for j, (k, con) in enumerate(constructors):
        v = defaults[k]
        text = texts.get(k, k.replace("_", " "))
        lab = tkinter.Label(f, text=text)
        widgets[k], vars[k] = con(f, v)
        lab.grid(row=j, column=0, sticky="w")
        widgets[k].grid(row=j, column=1, sticky="ew")

    def trace_pattern(*args):
        if vars['pattern'].get() > 1:
            widgets['bounded'].configure(state="normal")
            trace_bounded()
        else:
            widgets['bounded'].configure(state="disabled")
            widgets['contact_angle'].configure(state="disabled")

    def trace_bounded(*args):
        if vars['bounded'].get() != 0:
            widgets['contact_angle'].configure(state="normal")
        else:
            widgets['contact_angle'].configure(state="disabled")

    def trace_offset(*args):
        if vars['roughing_offset'].get() > 0:
            widgets['roughing_depth'].configure(state='normal')
        else:
            widgets['roughing_depth'].configure(state='disabled')
    vars['pattern'].trace('w', trace_pattern)
    vars['bounded'].trace('w', trace_bounded)
    vars['roughing_offset'].trace('w', trace_offset)

    trace_pattern()
    trace_bounded()
    trace_offset()

    status = tkinter.IntVar()
    bb = tkinter.Button(b, text=_("OK"), command=lambda:status.set(1), width=8, default="active")
    bb.pack(side="left", padx=4, pady=4)
    bb = tkinter.Button(b, text=_("Cancel"), command=lambda:status.set(-1), width=8, default="normal")
    bb.pack(side="left", padx=4, pady=4)
    
    app.bind("<Escape>", lambda evt: status.set(-1))
    app.bind("<Return>", lambda evt: status.set(1))
    app.wm_protocol("WM_DELETE_WINDOW", lambda: status.set(-1))
    app.wm_resizable(0,0)

    app.wait_visibility()
    app.tk.call("after", "idle", ("after", "idle", "focus [tk_focusNext .]"))
    #app.tk_focusNext().focus()
    app.wait_variable(status)


    for k, v in list(vars.items()):
        defaults[k] = v.get()

    app.destroy()

    if status.get() == -1:
        raise SystemExit(_("image-to-gcode: User pressed cancel"))

    pickle.dump(defaults, open(rc, "wb"))

    return defaults

def main():
    if len(sys.argv) > 1:
        im_name = sys.argv[1]
    else:
        import tkinter
        import tkinter.filedialog as tkFileDialog

        im_name = tkFileDialog.askopenfilename(defaultextension=".png",
            filetypes = (
                (_("Depth images"), ".gif .png .jpg"),
                (_("All files"), "*")))
        if not im_name: raise SystemExit
        tkinter._default_root.destroy()
        tkinter._default_root = None
    im = Image.open(im_name)
    size = im.size
    im = im.convert("L") #grayscale
    w, h = im.size

    nim = numpy.frombuffer(tobytes(im), dtype=numpy.uint8).reshape((h, w)).astype(numpy.float32)
    options = ui(im, nim, im_name)

    step = options['pixelstep']
    depth = options['depth']

    if options['normalize']:
        a = nim.min()
        b = nim.max()
        if a != b:
            nim = (nim - a) / (b-a)
    else:
        nim = nim / 255.0

    maker = tool_makers[options['tool_type']]
    tool_diameter = options['tool_diameter']
    pixel_size = options['pixel_size']
    tool = make_tool_shape(maker, tool_diameter, pixel_size)

    if options['expand']:
        if options['expand'] == 1: pixel = 1
        else: pixel = 0
        w, h = nim.shape
        tw, th = tool.shape
        w1 = w + 2*tw
        h1 = h + 2*th
        nim1 = numpy.zeros((w1, h1), dtype=numpy.float32) + pixel
        nim1[tw:tw+w, th:th+h] = nim
        nim = nim1
        w, h = w1, h1
    nim = nim * depth

    if options['invert']:
        nim = -nim
    else:
        nim = nim - depth

    rows = options['pattern'] != 1
    columns = options['pattern'] != 0
    columns_first = options['pattern'] == 3
    spindle_speed = options['spindle_speed']
    if rows: convert_rows = convert_makers[options['converter']]()
    else: convert_rows = None
    if columns: convert_cols = convert_makers[options['converter']]()
    else: convert_cols = None

    if options['bounded'] and rows and columns:
        slope = tan(options['contact_angle'] * pi / 180)
        if columns_first:
            convert_rows = Reduce_Scan_Lace(convert_rows, slope, step+1)
        else:
            convert_cols = Reduce_Scan_Lace(convert_cols, slope, step+1)
        if options['bounded'] > 1:
            if columns_first:
                convert_cols = Reduce_Scan_Lace(convert_cols, slope, step+1)
            else:
                convert_rows = Reduce_Scan_Lace(convert_rows, slope, step+1)

    units = unitcodes[options['units']]
    convert(nim, units, tool, pixel_size, step,
        options['safety_height'], options['tolerance'], options['feed_rate'],
        convert_rows, convert_cols, columns_first, ArcEntryCut(options['plunge_feed_rate'], .125),
        spindle_speed, options['roughing_offset'], options['roughing_depth'], options['feed_rate'])

if __name__ == '__main__':
    main()

# vim:sw=4:sts=4:et:

