widget for persons signature

198 views
Skip to first unread message

Ian Collins

unread,
Nov 14, 2015, 3:44:44 PM11/14/15
to Kivy users support
Hi,
I need a simple widget on my app for a person to draw their signature - and save results in an image file. Has anyone done this? (rather than me having to reinvent wheel).

History - my app produces a legal document that someone has to sign off - and the result is emailed. Just need the persons signature sent in the email.

Cheers,
Ian.


sebastián lópez

unread,
Jan 8, 2017, 2:11:55 PM1/8/17
to Kivy users support
I have to do the same, someone has a suggestion.

sebastián lópez

unread,
Jan 8, 2017, 4:36:35 PM1/8/17
to Kivy users support
Hi, I'm trying with this code
import os
from kivy import App
from kivy.uix.widget import Widget
from kivy.graphics import Color, Line

class MyPaintWidget(Widget):
    def on_touch_down(self, touch):
        color = (0,0,0)
        with self.canvas:
            Color(*color)
            d = 30.
            touch.ud['line'] = Line(points=(touch.x, touch.y), width=1.4)
    def on_touch_move(self, touch):
        touch.ud['line'].points += [touch.x, touch.y]
        #res = self.spline(touch.ud['line'].points, [touch.x, touch.y])
        #for point in res:
        #    touch.ud['line'].points += point
    def _exportImage(self):
        dirname = App.get_running_app().user_data_dir
        self.export_to_png(os.path.join(dirname, 'firma.png'))

And the kv file 

BoxLayout:
orientation:'vertical'
size_hint_y: None
StackLayout:
MDRaisedButton:
text: 'Borrar'
on_press: _painter_id.canvas.clear()
size: '49dp', '40dp'
MDRaisedButton:
text: 'Grabar'
on_press: _painter_id._exportImage()
size: '49dp', '40dp'
MyPaintWidget:  
id: _painter_id
        size:'300dp','300dp'
 

Where MDRaiseButton is from kivymd, 

But I have the following errors:
1. I should not be able to draw in top of the MDRaisedButton buttons, but the canvas allows me.
2. When I try to save the canvas then I obtain an empty png but with a bounding rectangle

I just want to save the canvas content without the bounding rectangle and prevent the user draws outside the control

sebastián lópez

unread,
Jan 8, 2017, 5:05:34 PM1/8/17
to Kivy users support
I've fixed the issue to prevent the user draws outside the control

class MyPaintWidget(Widget):
    def on_touch_down(self, touch):
        color = (0,0,0)
        with self.canvas:
            Color(*color)
            d = 30.
            if self.__valid(touch.x, touch.y):
                touch.ud['line'] = Line(points=(touch.x, touch.y), width=1.4)
            else:
                touch.ud['line'] = Line( width=1.4)
    def __valid(self, x,y):
        x0, y0 = self.pos# please change
        x1 = x0 + self.width
        y1 = y0 + self.height
        if x < x0:
            return False
        elif x > x1:
            return False
        if y < y0:
            return False
        elif y > y1:
            return False
        return True
    def on_touch_move(self, touch):
        x, y = touch.x, touch.y
        if self.__valid(x,y):
            touch.ud['line'].points += [x,y]

But I don´t know how to solve the related save issue

sebastián lópez

unread,
Jan 10, 2017, 2:37:58 PM1/10/17
to Kivy users support
Under android the error is missing, I just had to change the background color of canvas kv language

<widget>:
   canvas.before:
       Color:
           rgba:(1,1,1,1)
       Rectangle:
           pos:self.pos
           size: self.size

The next step would be to convert the png file white color into transparent one, by using pillow It would be easy but how to include pillow into kivy?

sebastián lópez

unread,
Jan 10, 2017, 4:31:24 PM1/10/17
to Kivy users support
Now I'm near to solve it, Here is the code

from kivy.core.image import Image
def remove_background_color(self, filename):
    im = Image(filename)
    for x in range(im.width):
        for y in range(im.height):
            if im.read_pixel((x,y)) == (1,1,1,1):
                im.set_pixel((x,y), (1,1,1,0)) ### the im hasn't set_pixel method
    im.save()

Someone has an idea?

sebastián lópez

unread,
Jan 15, 2017, 6:59:03 AM1/15/17
to Kivy users support
The widget can be improbed by softing the lines, in that case I used a bezier curve approach

The next code was test by using python 2.7.12 and kivy 1.9.1 and it's licenced under GPL V3.0

# studentdb.py

from kivy.uix.widget import Widget
from kivy.graphics import Color, Line
from PIL import Image as ImagePil

class BezierBuilder:
    def binomialCoeff(self, n, k):
        C = [[0 for x in range(k+1)] for x in range(n+1)]
        # Calculate value of Binomial Coefficient in bottom up manner
        for i in range(n+1):
            for j in range(min(i, k)+1):
                # Base Cases
                if j == 0 or j == i:
                    C[i][j] = 1
                # Calculate value using previosly stored values
                else:
                    C[i][j] = C[i-1][j-1] + C[i-1][j]
        return C[n][k]
    def Bernstein(self, n, k):
        "Bernstein polynomial"
        coeff = self.binomialCoeff(n, k)
        def _bpoly(x):
            return [coeff*xi**k*(1 - xi)**(n - k) for xi in x]
        return _bpoly
    def linspace(self, init=0, fin=1, num=200):
        delta =  (fin-init)/float(num)
        lista= [init+pos*delta for pos, val in enumerate(range(num+1))]
        lista[-1] = fin
        return lista
    def outer(self, vecX, vecY):
        data = vecX[:]
        for pos, x in enumerate(vecX):
            data[pos] = (x*vecY[0], x*vecY[1])
        return data
    def bezier(self, points, num=10):
        # points = [[x1,y1],[x2,y2], ... [xn, yn]]
        "Build Bézier curve from points."
        N = len(points)
        t = self.linspace(0, 1, num=num)
        #allocating space
        curve = [(0, 0) for i in range(num+1)]
        for ii in range(N):
            res = self.outer(self.Bernstein(N - 1, ii)(t), points[ii])
            #print('=========')
            #print('{},{}'.format(len(curve),len(res)))
            for pos, rowcontent in enumerate(res):
                curve[pos] = (curve[pos][0]+rowcontent[0], curve[pos][1]+rowcontent[1])
        return curve

class MyPaintWidget(Widget):
    has_border = False
    _line_border = None
    _bezier = BezierBuilder()
    def on_touch_down(self, touch):
        color = (0,0,0)
        with self.canvas:
            Color(*color)
            d = 30.
            if self.__valid(touch.x, touch.y):
                touch.ud['line'] = Line(points=(touch.x, touch.y), width=1.5)
            else:
                touch.ud['line'] = Line( width=1.4)
        if not self.has_border:
            self.__draw_border()
    def clear(self):
        self.canvas.clear()
        with self.canvas:
            Color(1,1,1)
            self.__draw_border()
    def __valid(self, x,y):
        x0, y0 = self.pos
        x1 = x0 + self.width
        y1 = y0 + self.height
        if x < x0 or x > x1:
            return False
        if y < y0 or y > y1:
            return False
        return True
    def __draw_border(self):
        x0, y0 = self.pos
        x1 = x0 + self.width
        y1 = y0 + self.height
        Punto = namedtuple('Punto', ['x','y'])
        p1 = Punto(x0,y0)
        p2 = Punto(x0,y1)
        p3 = Punto(x1,y1)
        p4 = Punto(x1,y0)
        with self.canvas:
            Color(1,1,1)
            self._line_border = Line(points=(p1.x, p1.y,
            p2.x, p2.y, p3.x, p3.y, p4.x, p4.y,
            p1.x, p1.y), width=2)
    def on_touch_move(self, touch):
        x, y = touch.x, touch.y
        if self.__valid(x,y):
            point = self.spline(touch.ud['line'].points, (x,y))
            for p in point:
                touch.ud['line'].points += p
    def _exportImage(self):
        # se borra el borde
        #if not(self._line_border is None):
        #   lin = self._line_border.points = []
        #  del(lin)
        dirname = App.get_running_app().user_data_dir
        filename = os.path.join(dirname, 'firma.png')
        self.export_to_png(filename)
        self.remove_background_color(filename)
    def remove_background_color(self, filename):
        #return
        im = Imagepil.open(filename) 
        im = im.convert('RGBA')
        pixdata = im.load()
        for y in range(im.size[1]):
            for x in range(im.size[0]):
                if pixdata[x,y] == (255,255,255,255):
                    pixdata[x,y]  = (255,255,255,0)
        im.save(filename,'PNG')
    def spline(self, points, newPoints):
        if len(points) < 4:
            return [newPoints]
        n = min([14, len(points)]) # 7 points 
        puntos = points[-n:]
        points[-n:] = []
        pt = list()
        while len(puntos) >1:
            pt.append((puntos.pop(0), puntos.pop(0)))
        pt.append(newPoints)
        return self._bezier.bezier(pt, num=20)

In the kv hand 

# studentdb.kv
MyPaintWidget:  
    id: _painter_id
    size_hint: None, None
    size:'500dp','300dp' # this should be changed depending on your requirements
   
    canvas.before:
        Color:
            rgba: 1, 1, 1, 1
        Rectangle:
            pos: self.pos
            size: self.size

Now we have a better sign widget. I decided not to include numpy in the calculations due to the added apk size

Finally please you have to remember to include the pillow library in the requirements

# buildozer.spec

...
requirements= pillow, <library_name_1>, <library_name_2> 
...

Fari Tigar

unread,
Apr 12, 2017, 10:15:02 AM4/12/17
to Kivy users support
Hi Sebastian,

I came a long with the same requirement and used your script to create a small app from it, maybe someone wants to play with it.
A simple function to save into a db/json or transfer over the network is added.

Would you consider to add it to kivy garden, I think its a nice feature.

#save_texture.py

from
kivy.app import App

from kivy.uix.widget import Widget
from kivy.graphics import Color, Line
from kivy.utils import get_color_from_hex
from kivy.core.window import Window
from kivy.properties import ObjectProperty
from kivy.interactive import InteractiveLauncher

from PIL import Image as ImagePil
from collections import namedtuple
import base64
import os

class BezierBuilder:
   
def binomialCoeff(self, n, k):
       
# http://www.geeksforgeeks.org/dynamic-programming-set-9-binomial-coefficient/
        C = [[0 for x in range(k + 1)] for x in range(n + 1)]
       
# Calculate value of Binomial Coefficient in bottom up manner
        for i in range(n + 1):
           
for j in range(min(i, k) + 1):
               
# Base Cases
                if j == 0 or j == i:
                    C
[i][j] = 1
                # Calculate value using previosly stored values
                else:

                    C
[i][j] = C[i - 1][j - 1] + C[i - 1][j]

       
return C[n][k]

   
def Bernstein(self, n, k):
       
"Bernstein polynomial"
        coeff = self.binomialCoeff(n, k)

       
def _bpoly(x):

           
return [coeff * xi ** k * (1 - xi) ** (n - k) for xi in x]


       
return _bpoly

   
def linspace(self, init=0, fin=1, num=200):
        delta
= (fin - init) / float(num)
        lista
= [init + pos * delta for pos, val in enumerate(range(num + 1))]
        lista
[-1] = fin
       
return lista

   
def outer(self, vecX, vecY):
        data
= vecX[:]
       
for pos, x in enumerate(vecX):

            data
[pos] = (x * vecY[0], x * vecY[1])

       
return data

   
def bezier(self, points, num=10):
       
# points = [[x1,y1],[x2,y2], ... [xn, yn]]
        "Build Bézier curve from points."
        N = len(points)
        t
= self.linspace(0, 1, num=num)

       
# allocating space
        curve = [(0, 0) for i in range(num + 1)]
       
for ii in range(N):
            res
= self.outer(self.Bernstein(N - 1, ii)(t), points[ii])

           
for pos, rowcontent in enumerate(res):

                curve
[pos] = (curve[pos][0] + rowcontent[0], curve[pos][1] + rowcontent[1])

       
return curve


class MyPaintWidget(Widget):
    has_border
= False
    _line_border = None
    _bezier = BezierBuilder()


   
def on_touch_down(self, touch):
        color
= (0, 0, 0)
       
with self.canvas:
           
Color(*color)
           
d = 30.
            if self.collide_point(*touch.pos):

                touch
.ud['line'] = Line(points=(touch.x, touch.y), width=1.5)
           
else:
                touch
.ud['line'] = Line(width=1.4)

       
if not self.has_border:
           
self.__draw_border()

   
def clear(self):
       
self.canvas.clear()
       
with self.canvas:
           
Color(1, 1, 1)
           
self.__draw_border()


   
def __draw_border(self):
       
return True
        x0, y0 = self.pos
        x1
= x0 + self.width
        y1
= y0 + self.height
       
Punto = namedtuple('Punto', ['x', 'y'])
        p1
= Punto(x0, y0)
        p2
= Punto(x0, y1)
        p3
= Punto(x1, y1)
        p4
= Punto(x1, y0)
       
with self.canvas:
           
Color(1, 1, 1)
           
self._line_border = Line(points=(p1.x, p1.y,
                                             p2
.x, p2.y, p3.x, p3.y, p4.x, p4.y,
                                             p1
.x, p1.y), width=2)

   
def on_touch_move(self, touch):

       
if self.collide_point(*touch.pos):
            point
= self.spline(touch.ud['line'].points, (touch.pos))

           
for p in point:
                touch
.ud['line'].points +=
 p

   
def exportImage(self, filename=r"firma.png"):
        dirname
= App.get_running_app().user_data_dir
        absolute_path
= os.path.join(dirname, filename)
       
print(absolute_path)
       
self.export_to_png(absolute_path)
       
# self.remove_background_color(absolute_path)
        # print encoded string
        print (self.image_encode64(absolute_path))

   
def remove_background_color(self, filename):
       
# return
        im = ImagePil.open(filename)

        im
= im.convert('RGBA')
        pixdata
= im.load()
       
for y in range(im.size[1]):
           
for x in range(im.size[0]):
               
if pixdata[x, y] == (255, 255, 255, 255):
                    pixdata
[x, y] = (255, 255, 255, 0)

        im
.save(filename)


   
def image_encode64(self,filename):
       
"""
       
        :param filename: image to encode in  
        :return: image as encoded string, eg. for saving into db or transport
                 over network
        """
        with open(filename, "rb") as image_file:
           
return base64.b64encode(image_file.read()).decode('utf-8')


   
def spline(self, points, newPoints):
       
if len(points) < 4:
           
return [newPoints]
        n
= min([14, len(points)])  # 7 points
        puntos = points[-n:]
        points
[-n:] = []
        pt
= list()
       
while len(puntos) > 1:
            pt
.append((puntos.pop(0), puntos.pop(0)))
        pt
.append(newPoints)
       
return self._bezier.bezier(pt, num=20)


   
def set_color(self, new_color):
       
self.last_color = new_color
       
self.canvas.add(Color(*new_color))


class SaveTextureApp(App):
   
pass


if __name__ == '__main__':
   
#Window.clearcolor = get_color_from_hex('#ffffff')
    SaveTextureApp().run()


#save_texture.kv
#:import C kivy.utils.get_color_from_hex
#:import  MyPaintWidget save_texture

BoxLayout:
    orientation
:"vertical"
   
MyPaintWidget:
        id
: painter_id

        canvas
.before:

           
Color:
                rgba
: 1, 1, 1, 1
           
Rectangle:
                pos
: self.pos
                size
: self.
size
   
BoxLayout:
        size_hint_y
:0.3
       
Button:
            text
:"clear"
            on_press
: app.root.ids.painter_id.clear()
       
Button:
            text
:"OK"
            on_press
: app.root.ids.painter_id.exportImage()


Reply all
Reply to author
Forward
0 new messages