You can find several tools for drawing handwritten sketches on your photos. However, I couldn't find a tool with a zoom function. Standard photos and markup were difficult to use due to the limited magnification. So I made it this time. I also created ui with a script, so it works with one file.
For those who can write the code bang bang, I am writing annotations that may make me angry. It's a memo for myself, but for those who are starting to study Python, the operation of the script is easy to follow. I'm leaving it. Also, as a result of trial and error, some unused functions are sleeping.
I started using the class and wrote it.
ipad + pythonista3
・ You can read photos and write handwritten sketches. ・ Several types of line colors and thicknesses are available. -Implemented the undo function. -Implementation of zoomin zoomout function. zoom starts from the upper left. -Implemented scrollview according to the implementation of the enlargement function.
-The implementation method of scrollview and the cooperation of scrolling and scaling. -The touch event and sxrollview conflict with each other in receiving the touch event. -Implementation of undo (stores handwritten lines in an array, erases the last line, and then redraws all lines)
epaint.py
#! python3
#You can load images and sketch with enlargement function
#
#
#20201228 ver000 Easypaint creation started.
#20201229 ver001 scroll view is implemented by viewing a large image and displaying it.
#20201231 ver002 botton menu added. Added display such as coordinates.
#20210101 ver003 path is created in another class. The touch event conflicts with scrollview in path. Receive only the front.
# 20210101 ver004 btn_lock etc. Switch between scv and pv touch enable to avoid touch conflict+Coordinate correction of pv with all frames.
#20210101 ver005 path The color and width are reflected in the drawing. The drawing position of path remains flying to the upper left. load was implemented several versions ago.
#20210102 ver006 path Fixed so that the drawing does not jump to the upper left even if it scrolls. I added the process of pouring offset into pv.
#20210102 ver007 save created.
#20210103 ver008 zoom created. The center position at zoom is slightly above the upper left. When drawing near the upper left, an error of painting the entire surface appears.
#20210105 ver009 Display image size, magnification, file name argument or epaint in the title. Fixed path drawing strange.
#20210105 ver010 save Fixed that the image is doubled by the magnification. Show save completion.
#20210106 ver011b b version. undo implementation. However, drawing is slow with the photo 3200x2400 size.
#20210107 ver012c c version. Fixed bugs such as speed improvement and undo thickness that are initialized. Code notes and old code cleanup.
#20210110 ver013c c version. Fixed a bug that the drawing position shifts in zoom after scrolling. Button position correction.
#Remaining
#
#Abandon the acquisition of the read photo file name. Only intermediate file names can be obtained. I want to save with the original file name + alpha.
#Abandon embedding the QR code and passing the original file name. The QR code can only be decoded via the camera.
#Passing the original file name by embedding steganography in the image is not good because of the operation speed. Do you work in separate threads?
#
#
import ui, os, sys, photos, scene
import Image, io
import datetime
def pil2ui(imgIn): # pil(jpg) => ui(PNG)pil and ios(ui)The image data used by is different, so conversion is required. This time I'm converting from pil to ui. imgIn=pil
with io.BytesIO() as bIO: #Convert pil image data to image data that can be used with ios ui. import io required
imgIn.save(bIO, 'PNG')
imgOut = ui.Image.from_data(bIO.getvalue())
del bIO
return imgOut
########################################
#Hand-painted on the screen. Since it conflicts with SCROLLVIEW, it needs to be in a different class. Only the front moves.
class PathView (ui.View):
def __init__(self):
#self.frame = frame
self.flex = 'WH'
self.frame=(0,40,1000,680)
self.color = 'red'
self.path_width = 3
self.path_color = 'red'
self.action = None
self.touch_enabled = True
self.path = None
self.paths = []
self.bpath = None
self.bpaths= []
self.scvoffset_x = 0
self.scvoffset_y = 0
self.scvrate = 1.0
self.image = None
self.base_image = None
self.image_w = 256
self.image_h = 256
def touch_began(self, touch):
x, y = touch.location
x2 = (self.scvoffset_x + x)/self.scvrate
y2 = (self.scvoffset_y + y)/self.scvrate
print('touch_began x '+str(x)+' y '+str(y))
print(' x2:'+str(x2)+' y2:'+str(y2)+' rate'+str(self.scvrate))
print('scv offset x'+str(self.scvoffset_x)+' y'+str(self.scvoffset_y))
self.path = ui.Path()#For screen drawing
self.path.line_width = self.path_width#This sentence only sets the initial value of the width. Fixed value is OK.
self.path.line_join_style = ui.LINE_JOIN_ROUND
self.path.line_cap_style = ui.LINE_CAP_ROUND
self.path.line_width = 3#
self.path.move_to(x, y)
self.bpath=ui.Path()#For drawing on images
self.bpath.line_width = self.path_width#This sentence only sets the initial value of the width. Fixed value is OK.
self.bpath.line_join_style = ui.LINE_JOIN_ROUND
self.bpath.line_cap_style = ui.LINE_CAP_ROUND
self.bpath.line_width = 0#The width is set to 0 so as not to draw on the screen.
self.bpath.move_to(x2, y2)
def touch_moved(self, touch):
x, y = touch.location
x2 = (self.scvoffset_x + x)/self.scvrate
y2 = (self.scvoffset_y + y)/self.scvrate
#print('touch_moved x ;'+str(x)+' y'+str(y))
#print('touch_moved x2;'+str(x2)+' y'+str(y2))
self.path.line_to(x, y)#If this is not executed, it will not be drawn on the image. However, when executed, it is temporarily drawn on the screen.
self.bpath.line_to(x2,y2)#Executes both screen drawing and image drawing, and hides the image drawing with a width of 0. Pass the width when drawing.
self.set_needs_display()
def touch_ended(self, touch):
print('touch_ended')
#Save the path in bpaths and use it for undo implementation. Since path is only a route, record the line color and line width as well.
self.bpaths.append((self.bpath, (int(self.image_w), int(self.image_h)), self.path_color,int(self.path_width)))
#Set action in view and call the drawing function. If you do not register it in acion, the function path of the upper class_I couldn't call action.
if callable(self.action): #callable checks if the function is executable. Maybe you don't need it?
self.action(self)#Since it is set to action of pv, path_I'm calling action.
#Initialize path for each brush.
self.path = None
self.bpath = None
self.set_needs_display()
#With this function, the touch is drawn on the screen.
def draw(self):
if self.path:
self.path.stroke()
self.bpath.stroke()
#line when realizing undo_I couldn't mess with width on the main program side, so run it on a pv instance
def pv_bpath_undo(self):
print('pv_path_undo')
bpath = None
if callable(self.action):
self.action(self)#We are calling the action of pv. path_action
bpath = None
########################################
#Main program
class epaint(ui.View):
def __init__(self,filename1):
self.name='easy paint'#View name display. Later, the magnification and xy coordinates ui will be displayed here.
self.btn_w=100#Button width
self.btn_h=40 #Button height
self.filename1 = filename1
self.path_color = 'red'
self.path_width = int(6)
self.scvoffset_x = int(0)#Origin position on the upper left of the drawing surface with respect to the original image
self.scvoffset_y = int(0)#Origin position on the upper left of the drawing surface with respect to the original image
self.scvrate = float(1.0)#Magnification of drawing surface with respect to original image
self.scvrate0= float(1.0)#For storing the drawing magnification before zooming
self.image_w,self.image_h=int(0),int(0)#Width and height of the original image
self.pathimage_x,self.pathimage_y=int(0),int(0)#Path coordinates of the original image
self.pathscv_x,pathscv_y=int(0),int(0)#Drawing surface path coordinates
self.paths = []#Store path as an array. (For undo)
self.path=None
self.base_image = ui.Image.named('test:Peppers')#The original image
self.image_w,self.image_h = self.base_image.size#Original image width and height pil.size method
w,h = ui.get_screen_size()#Get the screen size. The part excluding x and the class name.
self.scr_w,self.scr_h = w,h #Full screen width and height
self.biv = ui.ImageView()#base image view View of the original image
self.biv.frame = (0, 0,self.image_w,self.image_h)#Get the size of the original image. Upper left coordinates and lower right coordinates
#Upper left absolute coordinates x,Upper left absolute coordinates y,Bottom right x absolute coordinates,Lower right y absolute coordinates
#ui coordinates: The coordinates increase from the upper left to the lower right.
#scene coordinates: The coordinates increase from the lower left to the upper right. Same as so-called ordinary xy coordinates.
#
self.biv.image = self.base_image
self.biv.bg_color = 'red'
self.biv.center = (128,128)#Specify the coordinates to display the center of the view. Since the test image is 256x256, I set it to 128.
self.biv.transform = ui.Transform.scale(1,1)#Specify the magnification of the image to be displayed on biv. Specified in x and y directions.
#self.biv.image = ui.Image.from_data(pil2ui(photos.pick_image()))#It doesn't work. Is the image format different?
self.scv = ui.ScrollView()
#flame(The window size of the rscroll view that is actually displayed)Declare the width of
self.scvframesize=(0,self.btn_h+3,self.scr_w,self.scr_h-self.btn_h-80)
self.scv.frame = self.scvframesize#(0,self.btn_h+3,self.scr_w,self.scr_h-self.btn_h-80)
self.scv.content_size = (self.image_w,self.image_h)#Declare the size of the frame to put the original image
#self.scv.flex='WH' #
self.scv.scroll_enabled = False#False #Scrolling is True enabled=Move / False invalid=Stop. Default True
self.scv.touch_enabled = False#False Do not receive touch events. Touch and pass to the view below.
#Receive a True Touch event. Do not touch to the view below.
self.scv.add_subview(self.biv)#By putting biv in scv, it was assigned to the original image contents of scrollview.
self.add_subview(self.scv)#Declared that it is scv to be displayed by putting scv in self
self.btnscv = ui.ScrollView()#Create scrollview for menu button
self.btnscv.background_color = '#d9dcff'#Light purple
self.btnscv.frame = (0,0,self.scr_w,self.btn_h)
self.btnscv.content_size = (1300,self.btn_h)
self.btnscv.flex = 'W'#Fixed only in the width direction
self.add_subview(self.btnscv)
#Register the menu button. Repeating the same item is the function config below_I make it with button.
#config_button(button, name, frame, title)
self.scv_btn_lock = ui.Button()
self.config_button(self.scv_btn_lock, 'btn_lock',(5*self.btn_w,0,self.btn_w,self.btn_h), 'Lock')
self.scv_btn_load = ui.Button()
self.config_button(self.scv_btn_load, 'btn_load',(0*self.btn_w,0,self.btn_w,self.btn_h), 'Load')
self.scv_btn_save = ui.Button()
self.config_button(self.scv_btn_save, 'btn_save',(1*self.btn_w,0,self.btn_w,self.btn_h), 'Save')
self.scv_btn_undo = ui.Button()
self.config_button(self.scv_btn_undo, 'btn_undo',(4*self.btn_w,0,self.btn_w,self.btn_h), 'Undo')
self.scv_btn_color = ui.Button()
self.config_button(self.scv_btn_color, 'btn_color',(2*self.btn_w,0,self.btn_w,self.btn_h), 'Color')
self.scv_btn_path_width = ui.Button()
self.config_button(self.scv_btn_path_width, 'btn_path_width',(3*self.btn_w,0,self.btn_w,self.btn_h), '-')#No title because it is a button to specify the width of the line
self.scv_btn_zoomin = ui.Button()
self.config_button(self.scv_btn_zoomin,'btn_zoomin',(6*self.btn_w,0,self.btn_w,self.btn_h),'Zin')
self.scv_btn_zoomout = ui.Button()
self.config_button(self.scv_btn_zoomout,'btn_zoomout',(7*self.btn_w,0,self.btn_w,self.btn_h),'Zout')
self.lock_switch = 'lock'
self.colors = ['white', 'grey', 'red', 'green', 'blue', 'cyan', 'magenta', 'yellow']
self.color_nr = 2 #Specify the initial color of the red line
self.path_widths = [3, 6, 12, 24]
self.path_w_nr = 1 #6 Specify what the width of the line is
self.scv_btn_color.tint_color = self.colors[self.color_nr]
#Needed to pass touch to drawing(Perform drawing on the screen)However, the view receives the touch event only in the foreground.
#In other words, drawing and scroll view are incompatible. (Possible by replacing the front.It is also possible to switch touch enabled)
self.pv = PathView()#(frame=self.bounds)
self.pv.frame = self.scvframesize#(0,self.btn_h,self.scr_w,self.scr_h-self.btn_h)
self.pv.action = self.path_action
self.pv.color=self.colors[self.color_nr]
self.pv.path_width = self.path_widths[self.path_w_nr]
self.pv.scvoffset_x = self.scvoffset_x
self.pv.scvoffset_y = self.scvoffset_y
self.pv.scvrate = self.scvrate
self.add_subview(self.pv)
self.path_width_change()
self.image = None
self.set_btn_actions()#Added action to btn. (Since it can be registered only after reading the function, it was added later)
#
self.present('fullscreen')
###############################
#btn click no action def
def btn_lock(self,sender):
self.logp(sender,'btn_lock')
#Modify the scrollview element to lock / unlock the scroll
#self.scv.scroll_enabld = True#False #Scrolling is True enabled=Move / False invalid=Stop. Default True
if self.lock_switch == 'lock':
print('btn_lock to scroll')
self.lock_switch = 'scroll'
self.scv.scroll_enabled = True
self.scv_btn_lock.title = 'scroll'
#self.pv.send_to_back()#Move the view to the back. (If the front view is opaque, it will not be visible)
self.scv.touch_enabled = True#False Do not receive touch events. Touch and pass to the view below.
self.pv.touch_enabled = False#False Do not receive touch events. Touch and pass to the view below.
else:
print('btn_lock to lock')
self.lock_switch = 'lock'
self.scv.scroll_enabled = False
self.scv_btn_lock.title = 'lock'
#self.scv.send_to_back()#Send view to the back. Doing this hides the screen under the top view.
self.scv.touch_enabled = False#False Do not receive touch events. Touch and pass to the view below.
self.pv.touch_enabled = True#False Do not receive touch events. Touch and pass to the view below.
self.scvoffset_x,self.scvoffset_y = self.scv.content_offset
self.pv.scvoffset_x = self.scvoffset_x#Pour the current scv offset into the pv.
self.pv.scvoffset_y = self.scvoffset_y
def btn_load(self,sender):
print('btn_load')
self.scvrate = 1
self.scvrate0 =1
self.base_image = ui.Image.from_data(photos.pick_image(raw_data=True))
self.image_w,self.image_h = self.base_image.size#Original image width and height pil.size method
self.pv.image_w , self.pv.image_h = self.image_w , self.image_h
self.biv.frame = (0, 0,self.image_w,self.image_h)#Resize biv to original image size
self.scv.content_size = (self.image_w,self.image_h)#Declare the size of the frame to put the original image
w,h = ui.get_screen_size()#Get the screen size. The part excluding x and the class name.
self.scr_w,self.scr_h = w,h #Full screen width and height
self.scvrate0 = self.scvrate #*2
self.biv.image = self.base_image#Substitute the original image for biv.
self.zoom_set(sender)
#Display image size and magnification in title
self.name ='Size:' + str(int(self.image_w)) + ', ' + str(int(self.image_h))+' rate:'+str(self.scvrate)+' '+self.filename1
self.set_needs_display()
def btn_save(self,sender):
print('btn_save')
saveimage0 = self.ui2pil(self.biv.image)
nitiji_now = datetime.datetime.now() #Get the current date and time
file_nitiji = nitiji_now.strftime("%Y%m%d_%H%M%S" )#Pillow cannot use double-byte characters in the file name.
#Extract the date and time from the datetime function and assign it to the variable of the character to be displayed. If you suddenly enter the file name, processing may fail.
filename = file_nitiji+'.jpg'
print(str(self.image_w)+' , '+str(self.image_h))
#pil risize int required. ANTIALIAS is slow with a focus on performance.
saveimage = saveimage0.resize((int(self.image_w),int(self.image_h)),Image.ANTIALIAS)#for PIL
#Regardless of zoom, each xy is doubled, so it is reduced to the original size.
saveimage.save(filename, quality=95, optimize=True, progressive=True)#for PIL
#You need to save the image in the py folder before saving it to the camera roll.
filename2 = 'MemoCamera'+str(self.filename1)+str(file_nitiji)+'Draw.jpg'
#filename1=Arguments added to the constructor when calling the nyuuryoubunn class
os.rename(filename,filename2)#Create full-width file name instead of pillow
#print('Draw_filename2:',filename2)
photos.create_image_asset(filename2) #Save to camera roll.
#The file name is the path to the previously saved image file. Not a saved name.
#file deleate
os.remove(filename2)
#If you do not erase the JPEG generated in the py folder, the time stamp of the first file will be
#Delete the jpg in the py folder every time because it will continue to be inherited by the exif data of the photo.
#Display image size and magnification in title
self.name ='save success'
#self.close() #Enable when you want to close the script after saving.
def btn_undo(self, sender):
print('btn_undo')
self.path_undo(sender)
self.set_needs_display()
def btn_color(self, sender):
if self.color_nr < len(self.colors) - 1:
self.color_nr += 1
else:
self.color_nr = 0
self.scv_btn_color.tint_color = self.colors[self.color_nr]
self.path_color = self.colors[self.color_nr]
self.pv.path_color = self.path_color
self.path_width_change()
def btn_path_width(self, sender):
if self.path_w_nr < len(self.path_widths) - 1:
self.path_w_nr += 1
else:
self.path_w_nr = 0
self.path_width = self.path_widths[self.path_w_nr]
self.pv.path_width = self.path_width
self.path_width_change()
def btn_zoomin(self,sender):
print('btn_zoomin')
self.scvrate0= self.scvrate
self.scvrate = self.scvrate * 2
self.zoom_set(sender)
self.logp(sender,'zoomin')
def btn_zoomout(self,sender):
print('btn_zoomout')
self.scvrate0= self.scvrate
self.scvrate = self.scvrate * 0.5
self.zoom_set(sender)
self.logp(sender,'zoomout')
###############################
# sub tool
def pil2ui(imgIn): # pil(jpg) => ui(PNG)pil and ios(ui)The image data used by is different, so conversion is required. This time I'm converting from pil to ui. imgIn=pil
with io.BytesIO() as bIO: #Convert pil image data to image data that can be used with ios ui. import io required
imgIn.save(bIO, 'PNG')
imgOut = ui.Image.from_data(bIO.getvalue())
del bIO
return imgOut
#from pythonista forum
def ui2pil(self, image):
mem = io.BytesIO(image.to_png())
out = Image.open(mem)
out.load()
mem.close()
return out
#A function that sets buttons together. Since the same attribute is repeatedly registered, it is made into a function.
def config_button(self,button,name,frame,title):
button.name = name
button.frame = frame
button.title = title
button.border_width = 1
button.corner_radius = 2
button.border_color = 'blue'
button.font = ('<system-bold>',25)
#button.action = name#The function when the button is pressed is the same as the button name.
#However, it cannot be added here because the function to be called must be loaded first. Added later with another function.
self.btnscv.add_subview(button)#scroll view of menu button=Assign the button to btnscv as the original view.
#The button action cannot be declared without registering the action function, so register it last.
def set_btn_actions(self):
for subview in self.btnscv.subviews:#Specify the view that contains the button
if isinstance(subview, ui.Button):
subview.action = getattr(self, subview.name)
#Display the processed photo in imageview.
def imgv_pick():
imgIn = photos.pick_image()
imggg = pil2ui(imgIn) #The image format is converted from pil to ui below.
#sender.superview['photo1'].image = imggg #imegeview photo_now to ios(ui)Pass the format image and reflect it.
return imggg
#It fits the image to the window size.
##############################
# draw tool
#path_Change the line width image of the width button
def path_width_change(self):
with ui.ImageContext(self.btn_w, self.btn_h) as ctx:
ui.set_color(self.colors[self.color_nr])
path = ui.Path()
path.line_width = self.path_widths[self.path_w_nr]
path.line_join_style = ui.LINE_JOIN_ROUND
path.line_cap_style = ui.LINE_CAP_ROUND
path.move_to(20,20)
path.line_to(80,20)
path.stroke()
image = ctx.get_image()
#background_image is the ui method that puts the background image of the button
self.scv_btn_path_width.background_image = image
#An image that reflects the line color and line width is put in the line width button as a background image.
#There is no particular meaning.
def layout(self):
pass
#Drawing to biv with bpath converted to biv
def path_action(self, sender):
#path = sender.path
bpath= sender.bpath#Required when accessing attributes outside init within an instance such as pv. The function also needs a sender.
#In this case, "attributes outside init in the instance"=self.pv.bpath.line_width .However, an error will occur if it is not registered in the action of pv.
old_img = self.biv.image
width, height = self.image_w,self.image_h
#If you get the size of biv, you cannot use it because the upper left part of the image is cut off when the image is reduced.
self.logp(sender,'path_action')
#The path is drawn as an image. w h is the drawing range, so base_The size of imge. Not the biv size to be zoomed.
with ui.ImageContext(width, height) as ctx:
if old_img:
old_img.draw()
self.pv.bpath.line_width = self.path_width#Reflect the width of the line
ui.set_color(self.path_color)#Reflect the drawing color of path
bpath.stroke()
self.biv.image = ctx.get_image()
def path_undo(self,sender):
#bpath= sender.bpath
last_path_color = self.path_color
last_path_width = self.path_width
#bpaths Counting the number of lists. To redraw all bpaths again with undo.
path_count = len(self.pv.bpaths)
#print(self.pv.bpaths)
width, height = self.image_w,self.image_h
#print('w h '+str(width)+' , '+str(height))
if path_count > 0:
self.pv.bpaths.pop()#Delete the last element
path_count -= 1
self.biv.image = self.base_image #Return the biv image to no bpath.
for i in range(0, path_count):#Redraw the bpath from the beginning to the previous bpath.
self.path_width = self.pv.bpaths[i][3]#self.path_width#Reflect the width of the line
self.path_color = self.pv.bpaths[i][2]#
self.pv.bpath = self.pv.bpaths[i][0]
self.pv.pv_bpath_undo()#Bpt is generated in pv to reflect the line width
self.path_color = last_path_color
self.path_width = last_path_width
self.set_needs_display()
def zoom_set(self,sender):
scvw,scvh = self.scr_w,self.scr_h-self.btn_h-80#Get the size of scv
self.scvoffset_x,self.scvoffset_y = self.scv.content_offset#The position of the upper left origin of scv in biv
#Move the scv offset closer to the position when it was scaled up or down
newx=self.scvoffset_x * self.scvrate / self.scvrate0
newy=self.scvoffset_y * self.scvrate / self.scvrate0
self.biv.frame = (0,0,self.image_w*self.scvrate,self.image_h*self.scvrate)
self.scv.content_size = (self.image_w*self.scvrate,self.image_h*self.scvrate)#Frame size to put the original image
self.scvoffset_x,self.scvoffset_y = self.scv.content_offset#The position of the upper left origin of scv in biv
self.pv.scvoffset_x = self.scvoffset_x#Pour the current scv offset into the pv.
self.pv.scvoffset_y = self.scvoffset_y
self.pv.scvrate = self.scvrate
self.scv.content_size = (self.image_w*self.scvrate,self.image_h*self.scvrate)#Frame size to put the original image
self.scv.content_offset=(newx,newy)#Move the scv offset to a position closer to the original position. Upper left reference.
self.scvoffset_x,self.scvoffset_y = self.scv.content_offset
self.pv.scvoffset_x = self.scvoffset_x#Pour the current scv offset into the pv.
self.pv.scvoffset_y = self.scvoffset_y
self.logp(sender,'zoom_set')
#Display image size and magnification in title
self.name ='Size:' + str(int(self.image_w)) + ', ' + str(int(self.image_h))+' rate:'+str(self.scvrate)+' '+self.filename1
#log print
def logp(self,sender,memo):
print(str(memo)+' biv.w'+str(self.biv.width)+' h'+str(self.biv.height)+' : offset'+str(self.scv.content_offset)+' rate'+str(self.scvrate)+' rate0:'+str(self.scvrate0))
###############################
#v = epaint()
#v.present('fullscreen')
if __name__ == "__main__":
epaint('epaint')#I want to take filename1 as an argument.
Recommended Posts