#!/usr/bin/env python

#   Gimp-Python - allows the writing of Gimp plugins in Python.
#   Copyright (C) 1997  James Henstridge <james@daa.com.au>
#
#   This program 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.
#
#   This program 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 this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

# Copyright (C) 2008  Paul Taney
# bug reports or suggestions to paultaney@yahoo.com
# my examples were
#    retinex_plugin.py  -- Copyright (C) 2007, John Fremlin
#    clothify.py plugin -- Copyright (C) 1997, James Henstridge
#    foggify.py plugin  -- Copyright (C) 1997, James Henstridge
#    average_layer.py plugin -- Copyright (C) 2008, Elmar Hoefner
#    gimppath2svg.py tool -- Copyright (C) 2000, Simon Budig

# my data files are jpg stripcharts of weather data about 3-5 megabytes.
# if your data is different from that, I have not tested with it and 
# you can expect the program to fail somehow.


TESTING = 0
if not TESTING:
    from gimpfu import *
import sys, os.path
import numpy, random

# in the future I hope to use scipy clustering
# from scipy.cluster.vq import *  # has kmeans2 (http://hackmap.blogspot.com/2007/09/k-means-clustering-in-scipy.html)


if not TESTING:
    gettext.install("gimp20-python", gimp.locale_directory, unicode=True)


line_template ="""
<line x1="%s" y1="%s" x2="%s" y2="%s"
style="stroke:rgb(99,99,99);stroke-width:2"/>
"""
  
header1 = """<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" 
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

<svg width="100%" height="100%" version="1.1"
xmlns="http://www.w3.org/2000/svg">

<!-- This SVG was generated by gimpfu plug-in at <Images>/Filters/Render/Stroke to SVG... -->
"""
  
header2 = """<!-- This is an SVG snippet representing a line. -->
<!-- This should not be considered a complete SVG file. -->
<!-- For instance, we dont handle line color (much). -->
"""

footer = """</svg>
"""

def getSVG(x1, y1, x2, y2):
    d = {"x1":str(x1), "y1":str(y1), "x2":str(x2), "y2":str(y2)}
    x1=str(x1); y1=str(y1); x2=str(x2); y2=str(y2)
    return line_template % (x1, y1, x2, y2)

def thin(L, density):
    """pop random points until density is... density"""
    if len(L) <= density:
        return L
 
    while len(L) > density:
        r = random.randint(1,len(L)-1)
        L.pop(r)
    return L

def unzip(L):
    """change 2-tuples to 4-tuple s for linedrawing""" 
    M = []
    while len(L)>=2:
        M.append((L[0][0], L[0][1], L[1][0], L[1][1])) 
        L = L[1:]
    return M

def writeSVG(filename, line, sx, sy, height=0):
    if 1: print "writeSVG:: writing %s, len(line)=%i" % (filename, len(line))

    h = height*sx

    acc = []
    X, Y = 0, 1
    for i in range(len(line)-1):
    #for x1, y1, x2, y2 in unzip(line):
        if height:
            x1, y1, x2, y2 = line[i][X]*sx, h-line[i][Y]*sy, line[i+1][X]*sx, h-line[i+1][Y]*sy  # have to flip y (for svg only?)
        else:
            x1, y1, x2, y2 = line[i][X]*sx, line[i][Y]*sy, line[i+1][X]*sx, line[i+1][Y]*sy 
        acc.append(getSVG(x1, y1, x2, y2))

    FH = open(filename, 'w')
    FH.write(header1)
    #FH.write(header2)
    FH.write("<!-- scalefactors  %.2f, %.2f -->\n" % (sx, sy))
    FH.write(" ".join(acc))
    FH.write(footer)
    FH.close()
    #pdb.gimp_message("wrote %s" % filename)

def writeCSV(filename, line, sx, sy, height=0):
    if 1: print "writeCSV:: writing %s, len(line)=%i" % (filename, len(line))

    h = height*sx 

    acc = []
    for x, y in line:
        if height:
            acc.append("%.2f, %.2f," % (x*sx, h-y*sy))  # have to flip y (for svg only?)
        else:
            acc.append("%.2f, %.2f," % (x*sx, y*sy))

    FH = open(filename, 'w')
    FH.write("# CSV file generated by stroke_to_vector.py\n")
    FH.write("# a gimpfu plug-in at <Images>/Filters/Render/Stroke to SVG\n")
    FH.write("# scalefactors  %.2f, %.2f\n" % (sx, sy))
    FH.write("\n".join(acc))
    FH.write("\n")
    FH.close()
    #pdb.gimp_message("wrote %s" % filename)

def write_tuple(filename, line, sx, sy, height=0):
    if 1: print "write_tuple:: writing %s, len(line)=%i" % (filename, len(line))

    h = height*sx  

    acc = []
    for x, y in line:
        if height:
            acc.append("    (%.2f, %.2f)," % (x*sx, h-y*sy)) 
        else:
            acc.append("    (%.2f, %.2f)," % (x*sx, y*sy))

    FH = open(filename, 'w')
    FH.write("# file generated by stroke_to_vector.py\n")
    FH.write("# a gimpfu plug-in at <Images>/Filters/Render/Stroke to SVG...\n")
    FH.write("# scalefactors  %.2f, %.2f\n" % (sx, sy))
    FH.write("line = (\n")
    FH.write("\n".join(acc))
    FH.write("\n    )\n")
    FH.close()
    #pdb.gimp_message("wrote %s" % filename)


def vanderwalt(image, f):
    """thanks to Stefan van der Walt"""
    RED, GRN, BLU = 0, 1, 2
    bluemask = (image[...,BLU] > f*image[...,GRN]) & \
               (image[...,BLU] > f*image[...,RED])

    return bluemask

def estimator(a, b, density):
    """estimate factor for next iteration. Tricky because b[LEN] holds the larger line length"""
    print "begin estimator: a_guess=(%.5f, %i)  b_guess=(%.5f, %i)  density=%i" % (a[0], a[1], b[0], b[1], density)
    TEST2 = True
    a_f, a_len = float(a[0]), float(a[1])
    b_f, b_len = float(b[0]), float(b[1])

    assert(a_f - b_f >0)
    assert(a_len - b_len <0)
    fraction = (b_len + a_len) / 2.0 / b_len

    # a little algebra, and it is giving me fits.  (I am in the wrong line of work)
    #if TEST2: print "fraction= %.5f;  fraction= (%i - %i) / (%i - %i)" % (fraction, b_len, density, b_len, a_len)  # wrong
    #if TEST2: print "fraction= %.5f;  fraction= (%i - %i) / %i" % (fraction, b_len, a_len, density)  # wrong
    #if TEST2: print "fraction= %.5f;  fraction= abs((%i - %i) / (%i - %i)  # density / range" % (fraction, b_len, density, a_len, density)
    #if TEST2: print "fraction= %.5f;  fraction= abs((%i - %i) / (%i - %i)  # density / range" % (fraction, a_len, density, b_len, density)
    #if TEST2: print "fraction= %.5f;  fraction= (%i - %i) / %i" % (fraction, b_len, a_len, b_len) # median for now...
    if TEST2: print "fraction= %.5f;  fraction= (%i + %i) / 2.0 + %i" % (fraction, b_len, a_len, a_len) # median for now...
    assert(fraction <1 and fraction >0)
    if fraction == 1: 
        print "estimator: convergence on density=%i" % density
        return "estimator: convergence!"
    guess = ((a_f - b_f) * fraction) + b_f
    if TEST2: print "guess= %.5f" % (guess)
    return guess


def successive_approximation(image, density, width, height, bpp):
    """keep calling vanderwalt till line length is within 10% of density target"""
    init_high = 1.400001
    init_low = 1.399991
    #print "successive_approximation: initializing high_guesses with f=%f, length=%i" % (init_high, width*height*bpp)
    #print "successive_approximation: initializing low_guesses with f=%f, length=%i" % (init_low, 0)
    high_guesses = [(init_high, width*height*bpp)]  # we collect recent stats in these
    low_guesses  = [(init_low, 0)]
    factor = 1.4
    seen_f_high = seen_f_low = False
    count = 0; failsafe = 9
    while 1:
        bluemask = vanderwalt(image, factor)
        line = numpy.array(bluemask.nonzero()).swapaxes(0,1).tolist()

        xlength = len(line)  # the returned list is of length 2.  a[x,y] 
        # spread the search outward from initial conditions until we cross target
        if xlength > density:
            """too many so raise the bar"""
            print "LOOP1.%i: line length = %i, factor too low =%.5f" % (count, xlength, factor)
            seen_f_low = True
            low_guesses.append((factor, xlength))
            factor += .5  # moving faster than the other guess because there is no upper limit
                          # while the f moving down has to stop at 0
        else:
            """not enuf so lower the bar"""
            print "LOOP1.%i: line length = %i, factor too high=%.5f" % (count, xlength, factor)
            seen_f_high = True
            high_guesses.append((factor, xlength))
            factor -= 0.1  # magic number, sorry. should get to .4 before failsafe (set to .174 max)
        if seen_f_high and seen_f_low:   # crossed_target
            break
        count += 1
        #print "count=%i, factor=%f\nhigh_guesses=%r\nlow_guesses=%r" % (count, factor, high_guesses, low_guesses)
        if count == failsafe:
             print "failsafe: level 1 failed to converge."
             break

    if count < failsafe:
        # plus or minus 5 percent
        while not ((xlength < density*1.05) and (xlength > density*.95)):
            # now we can use the estimator to converge quickly

            # note: bluemask = vanderwalt(original, factor)  error: gimp.Image object is unsubscriptable
            #       bluemask = vanderwalt(drawable, factor)  error: gimp.Layer object is unsubscriptable

            factor = estimator(high_guesses[-1], low_guesses[-1], density)
            # overloaded var...
            if factor == "estimator: convergence!":
                break
            bluemask = vanderwalt(image, factor)
            line = numpy.array(bluemask.nonzero()).swapaxes(0,1).tolist()
            xlength = len(line)
            if xlength > density:
                """too many so raise the bar"""
                print "LOOP2.%i: line length = %i, factor too low =%.5f" % (count, xlength, factor)
                low_guesses.append((factor, xlength))
            elif xlength < density:
                """not enuf so lower the bar"""
                print "LOOP2.%i: line length = %i, factor too high =%.5f" % (count, xlength, factor)
                high_guesses.append((factor, xlength))
            else:
                break  # converged!
            count += 1
            #print "count=%i, factor=%f\nhigh_guesses=%r\nlow_guesses=%r" % (count, factor, high_guesses, low_guesses)
            if count == failsafe * 2:
                 print "failsafe level 2. algorithm failed.\nPerhaps there are not %i uniquely colored pixels set to our color..." % density
                 break
    else:
        pdb.gimp_message("Not enuf blue pixels for this algorithm.")
        # xxx FIX THIS:: dont return the line because we might have to swapaxes. WRONG
    return bluemask
# end successive_approximation

def progress_update(stage, proportion_done):
    gimp.progress_update(proportion_done*0.10)

#def retinex_plugin(img, drawable, new_layer_name, opacity, logscale, flatten):
def stroke_to_vector_plugin(original, drawable, new_layer_name, scalefactorx, scalefactory, density, swap, outputformat, filename):

    #assert not pdb.gimp_selection.is_empty  # perhaps they must select an area for me to get the "drawable"?
    #[ width, height, channels ] = image.shape
    flatten = False
    density = int(density)

    width, height = drawable.width, drawable.height

    if drawable.has_alpha:
        bpp = 4
    else:
        bpp = 3

    original.undo_group_start()

    try:
       
        pr = drawable.get_pixel_rgn(0, 0, width, height, False)  # its just one channel afaict
        a = numpy.fromstring(pr[:,:], "B")
        assert(a.size == width * height * bpp)
        image = numpy.array(a.reshape(height,width,bpp),"B") # [:,:,0:min(bpp,3)]  # Travis O. page 133

        mask = successive_approximation(image, density, width, height, bpp)
        line = numpy.array(mask.nonzero()).swapaxes(0,1).tolist()

        #if fli_yy:
        #    M = []
        #    for x, y in line:
        #        M.append((xxx,xxx))
        #    line = M

        TEST3 = True
        if TEST3:
            # note:  if you dont swapaxes it will be 2 by n every time. See
            # http://www.scipy.org/Numpy_Example_List_With_Doc
            line = numpy.array(mask.nonzero()).tolist()
            print "no swap: len(line)= >>>%s<<<" % (len(line)) # 2
            line = numpy.array(mask.nonzero()).swapaxes(0,1).tolist()
            print "swapped: len(line)= >>>%s<<<" % (len(line)) # 967

        if len(line) == 2:  # [[xa], [ya]]
            pdb.gimp_message("failure near line 384, len(line)==%i.  please file a bug report." % (len(line)))
        else:
            if filename == None:
                filename = "tmp." + outputformat
                print "calling write function with filename = >>>%s<<<" % filename
                pdb.gimp_message("the filename will be  %s" % (filename))

            if 1: print "outputformat= >>>%s<<<" %  outputformat
            if 1: print "calling write function, len(line)= >>>%s<<<" % (len(line))
            if outputformat == "svg":
                # CAUTION: thin decimates the line so ymmv.  In the tests we call thin(line[:], d),  which copies the line
                #writeSVG(filename, thin(line, density), scalefactorx, scalefactory, height=0)
                writeSVG(filename, line, scalefactorx, scalefactory, height=0)
            elif outputformat == "py":
                #write_tuple(filename, thin(line, density), scalefactorx, scalefactory, height=0)
                write_tuple(filename, line, scalefactorx, scalefactory, height=0)
            elif outputformat == "csv":
                #writeCSV(filename, thin(line, density), scalefactorx, scalefactory, height=0)
                writeCSV(filename, line, scalefactorx, scalefactory, height=0)
            else:
                sys.stdout.write("No output format selected. This is a bug, as svg is the default.\n")
                pdb.gimp_message("No output format selected! (line 381)")
                #sys.exit(0)

        #opacity = 100
        #new_layer = gimp.Layer(bluemask, new_layer_name, width, height, 
                               #RGB_IMAGE, opacity, NORMAL_MODE)  # error: must be a gimp.Image, not a numpy.array

        David_says = """You are passing an RGB array to this function. Are you certain that
        your destination drawable does not have an alpha channel (thus requiring
        you to pass an RGBA array)?"""

        OUT = """
        if drawable.has_alpha:
            rgbmask = bluemask*bpp  # gimp.Image(width, height, RGBA_IMAGE)
            new_layer = gimp.Layer(rgbmask, new_layer_name, width, height, 
                               RGBA_IMAGE, opacity, NORMAL_MODE)
        else:
            rgbmask = bluemask*bpp  # gimp.Image(width, height, RGB_IMAGE)
            # first arg must be a gimp.Image
            new_layer = gimp.Layer(rgbmask, new_layer_name, width, height, 
                               RGB_IMAGE, opacity, NORMAL_MODE)"""

        # pr = new_layer.get_pixel_rgn(0, 0, width, height, True)
        # the pr[] is in the drawable"s address space afaict. we blit into the pr but flush the drawable.
        # blit!
        #pr[:,:] = (bluemask*bpp).tostring()  # error: wrong length
        #pr[:,:] = bluemask.tostring()  # rgbmask?
        #print ">>>%i, %i<<<" % (pr[:,:].shape) # string object has no attribute "shape"
        print "len(pr[:,:])= >>>%i<<<" % (len(pr[:,:]))


        # data = numpy.concatenate((numpy.zeros(half_window), data, numpy.zeros(half_window)))
        if drawable.has_alpha:
            # drawable.set_pixel_rgn(numpy.concatenate(mask, mask, mask, mask)) 
            # error:  'gimp.Layer' object has no attribute 'set_pixel_rgn'
            pass
        else:
            # drawable.set_pixel_rgn(numpy.concatenate(mask, mask, mask)) 
            pass

        # from arclayer.py:  (FC book, p219)
        drawable.flush()
        drawable.update(0,0,width,height)

        #original.add_layer(new_layer, 0)

        if flatten:
            original.flatten()

    finally:
        original.undo_group_end()


if not TESTING:
        register(
        "python-fu-stroke-to-vector",  # name
	"Render bluest pixels to a new layer and optionally save vector. (v0.1)",  # blurb
        N_("""Retain the bluest pixels in a new layer and save as a vector
where blue sub-pixels are greater than red and green by some factor 
(default 40%).  Why just blue?  because I am handling stripcharts with
blue ink on them.  If you have another application, I could add RADIOs."""),  # help
        "Paul Taney",  # author
        "Paul Taney",  # copyright
        "2008",  # date
        N_("Stroke To SVG..."),  # gimp menu
        "RGB*",  # image arguments?
        [
            (PF_IMAGE, "image", "Input image", None),
            (PF_DRAWABLE, "drawable", "Input drawable", None),
            (PF_STRING, "name", _("New _layer name"), _("blueline")),
            #(PF_SLIDER, "opacity", _("Op_acity"),    25, (0, 100, 1)),
            #(PF_SLIDER, "factor", _("How blue?"),    14, (10, 20, .5)),
            #(PF_BOOL,   "flatten", _("_Flatten layers together after processing"), False),
            (PF_FLOAT,  "scalefactorx", "scale factor x",    ".4"),    	
            (PF_FLOAT,  "scalefactory", "scale factor y",    ".4"),    	
            (PF_SLIDER, "density", _("Line _density"),    1500, (100, 3000, 100)),
            (PF_BOOL,   "flip_y", _("Flip y?"), False),
	    (PF_RADIO,  "outputformat", "Output format:",  "svg", (
		("None", "None"),
		("SVG",  "svg"),
		("CSV",  "csv"),
		("python tuple", "py"))), 	
            #(PF_FILENAME, "filename", "Output file:", os.path.expanduser("~/tmp.svg")),  # Beazley p326, fails trying to convert to Unicode...
            (PF_FILENAME, "filename", "Output file:", "tmp.svg"), 
        ],
        [],
        stroke_to_vector_plugin,
        menu="<Image>/Filters/Render/",
        domain=("gimp20-python", gimp.locale_directory)
        )

if not TESTING:
    main() # gimpfu
else:
    L = [(25, 26), 
         (50, 51), 
         (110, 101), 
         (170, 151), 
         (230, 201), 
         (290, 251), 
         (350, 301), 
         (410, 351), 
         (470, 401),
         (630, 351),
         (690, 401), 
         (150, 551), 
         (710, 601),
         (770, 651),
         (830, 701), 
         (890, 751), 
         (950, 801),
         (1010, 851),
         (1070, 901), 
         (1230, 951), 
         (1240, 1051),
         ]

    #getSVG(25, 26, 50, 51)
    h = 1100
    #density = 1
    #writeSVG("tmp1.svg", [(25, 26), (50, 51), (100, 101)], 1, 1, density)
    density = 3
    writeSVG("tmp3.svg", thin(L[:], density), 1, 1, height=0)
    density = 4
    writeSVG("tmp4.svg", thin(L[:], density), 1, 1, height=0)
    density = 5
    writeSVG("tmp5.svg", thin(L[:], density), 1, 1, height=0)
    density = 6
    writeSVG("tmp6.svg", thin(L[:], density), .2, .4, height=0)
    density = 18
    writeSVG("tmp18.svg", thin(L[:], density), .2, .4, height=0)
    writeSVG("not_thin18.svg", L[:], 1, 1, height=0)

    density = 3
    write_tuple("tmp3.py", thin(L[:], density), 1, 1, height=0)
    density = 4
    write_tuple("tmp4.py", thin(L[:], density), 1, 1, height=0)
    density = 5
    write_tuple("tmp5.py", thin(L[:], density), 1, 1, height=0)
    density = 6
    write_tuple("tmp6.py", thin(L[:], density), .2, .4, height=0)
    density = 18
    write_tuple("tmp18.py", thin(L[:], density), .2, .4, height=0)
    print "len(L= >>>%i<<<" % (len(L))
    write_tuple("not_thin18.py", L[:], 1, 1, height=0)

    density = 6
    #writeCSV("tmp6.csv", thin(L[:], density), 1, 1, height=0)
    density = 8
    #writeCSV("tmp8.csv", thin(L[:], density), .2, .4, height=0)
    density = 18
    #writeCSV("tmp18.csv", thin(L[:], density), .2, .4, height=0)
    #writeCSV("not_thin18.csv", L[:], 1, 1, height=0)

Note = """
find . -name \*scm -exec grep gimp-image {} \; | grep -v width | grep -v heig | perl -ne '@a=split; print "$a[0]\n"' | sort -d | uniq >
ALL_IMAGE_CMDS.txt 
(gimp-context-set-background
(gimp-drawable-fill
(gimp-drawable-set-visible
(gimp-image-add-channel
(gimp-image-add-hguide
(gimp-image-add-layer
(gimp-image-add-vguide
(gimp-image-clean-all
(gimp-image-convert-indexed
(gimp-image-convert-rgb
(gimp-image-crop
(gimp-image-delete
(gimp-image-delete-guide
(gimp-image-flatten
(gimp-image-lower-layer
(gimp-image-lower-layer-to-bottom
(gimp-image-merge-down
(gimp-image-merge-visible-layers
(gimp-image-raise-layer
(gimp-image-remove-channel
(gimp-image-remove-layer
(gimp-image-resize
(gimp-image-scale
(gimp-image-set-active-channel
(gimp-image-set-active-layer
(gimp-image-set-colormap
(gimp-image-set-component-active
(gimp-image-set-filename
(gimp-image-undo-disable
(gimp-image-undo-enable
(gimp-image-undo-group-end
(gimp-image-undo-group-start
"""
