Word 轉 手寫體軟件開發

介紹與準備

Tkinter 指南

這裏使用的是 Tkinter 來開發一款軟件的,如果不是用 Tkinter,請移步。
首先推薦一些 Tkinter 的教程吧:
Tkinter GUI 01
Tkinter GUI 02
Tkinter GUI 03
Tkinter GUI 04
Tkinter GUI 05
Tkinter GUI 06
Tkinter GUI 07
Tkinter GUI 08

然後,是一些有用的參考資料和書籍的下載地址:
Python GUI Tkinter 參考資料

再安裝如下模塊;

  • handright
  • docx:用於打開 Word
  • PIL:用於保存圖像,創建手寫字體模板

開發環境

這裏使用 Eclipse 的 PyDev 開發我們的 GUI 應用,有關 Eclipse 的內容,可以參閱:
Eclipse 安裝教程、PyDev 插件下載教程與有關設置

另外 PyDev 由於是一個插件,所以,其版本更新需要我們重新卸載、再安裝。讀者們下載了 PyDev 插件之後,過一陣子,就會彈出下面的一個對話框:
PyDev 更新提示

Word 轉手寫體代碼

首先,我們忽略 GUI,看一下我們的 Word 轉手寫體代碼。首先,在 Eclipse 中新建一個 Python 的項目:
新建Python 項目

主程序

首先,在我們新建的項目中,添加一個 .py 文件,如下所示:
添加一個 .py 文件

背景圖像

爲了讓 Word 轉換起來更像是手寫體,我們還要添加一個背景,這裏用的背景是我們童年的回憶——貓狗本,如下所示。將這個背景圖像,命名爲 background_01.jpg,並放在我們項目文件的 src(需要自己新建)文件中。
貓狗本背景,命名爲 background_01.jpg
放在項目文件中的src文件夾中

字體下載與設置

首先要轉換成手寫體,我們需要用一個·手寫體的字體,再將其加入隨機擾動,來模擬手寫體的效果。

大家可以從這個網站上下載一些手寫體,免費的:

  • 手寫體下載地址
  • 推薦一款瘦金體:http://www.zhaozi.cn/html/fonts/china/ruizibige/2020-05-25/26424.html

然後,同樣在項目文件夾中,添加一個 font 文件夾。然後,將下載完的手寫體字體,放到文件夾中。我大概是下載了這麼多的手寫體字體,大家可以參考一下:
手寫體字體下載圖片

主程序啦!!

之後,我們要往這個文件中,加入我們的主要程序代碼,如下所示:

from PIL import Image, ImageFont
import numpy as np
from handright import Template, handwrite
from multiprocessing import Pool
import time
text = """
鳥飛鵝跳,月上中梢,目上硃砂,已異非巳,勺旁傍白,萬事開頭,工戈不全,雨下摯友,稱斷人和
"""

background=Image.open(r'./src/background_01.jpg')
width, height = background.size
background = background.resize((np.int(3*width),np.int(2.5*height)),resample=Image.LANCZOS)

if __name__ == "__main__":
    time_start = time.time()    #計時開始
    template = Template(
        background=background,   #選擇北京
        font_size=100,    #選擇字體大小
        font=ImageFont.truetype(r".\font\瘦金簡體.ttf"),  #選擇字體
        line_spacing=150,
        fill=0,  # 字體“顏色”
        left_margin=250,    #選擇字體於背景邊緣的間距。
        top_margin=-30,
        right_margin=100,
        bottom_margin=100,
        word_spacing=15,
        line_spacing_sigma=6,  # 行間距隨機擾動
        font_size_sigma=1,  # 字體大小隨機擾動
        word_spacing_sigma=3,  # 字間距隨機擾動
        end_chars=",。;、“” ",  # 防止特定字符因排版算法的自動換行而出現在行首
        perturb_x_sigma=3,  # 筆畫橫向偏移隨機擾動
        perturb_y_sigma=3,  # 筆畫縱向偏移隨機擾動
        perturb_theta_sigma=0.03,  # 筆畫旋轉偏移隨機擾動
    )
    with Pool() as p:    #多線程,加快程序的運行。
        images = handwrite(text, template, mapper=p.map)
    for i, im in enumerate(images):    #輸出結果
        assert isinstance(im, Image.Image)
        im.save(r".\output\{}.jpg".format(i))    #將輸出結果保存到路徑中
    
    time_end = time.time()    #計時結束
    print(time_end-time_start)    #輸出運行時間

這裏要注意,im.save(r".\output\{}.jpg".format(i)) 將手寫體弄成 jpg 圖片之後,就會保存到一個叫 output 的文件夾中,並以 0 開始,命名圖像。這裏的 output 文件夾,要事先創建好,否則報錯。當然,也可以添加一條代碼來防止這樣的情況。這條代碼是什麼呢?評論區走起~~

代碼測試與結果

然後,運行一下我們的代碼,會跳出一下錯誤

File “E:\java-2020-03\eclipse\workspace\Word2write_Principle\principle1.py”, line 7
SyntaxError: Non-UTF-8 code starting with ‘\xc4’ in file E:\java-2020-03\eclipse\workspace\Word2write_Principle\principle1.py on line 8, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

這是因爲,我們的代碼的第7句,text = """ 鳥飛鵝跳,月上中梢,目上硃砂,已異非巳,勺旁傍白,萬事開頭,工戈不全,雨下摯友,稱斷人和 """出現了中文,所以識別不出來。只要在代碼的開頭,加入一句 #coding=gbk或者# -*- coding:utf-8 -*,再次運行即可。

效果如下:
運行結果
怎麼樣?心曠神怡對不對?除了有點東倒西歪以外,基本上是滿足了我們的需求啊。大家也可以用其他字體,來體驗一下效果。這裏用李國夫手寫體,來實驗一下,效果如下:
李國夫手寫體效果

鏈接 Word 文檔

我們的需求是,輸入一個 Word 文檔,再將其轉換爲手寫體。而不是每一次運行,都要設置 text 變量。爲了實現這個目的,我們需要另外寫一個程序,來讀寫 Word 文檔。

從 Word 文檔獲取輸入的代碼

再次新建一個 .py 文件,命名爲 principle2.py ,並鍵入如下代碼:

# coding: utf-8
import docx
def read_docx(docx_path = r'./input/demo.docx'):
    docStr = docx.Document(docx_path)
    txt = []
    for para in docStr.paragraphs:    # 如果檢測到 word 中有居中的,就在兩邊加上空格。
        parStr = para.text
        if_center = para.paragraph_format.alignment
        if if_center:
            parStr = parStr.center(48)
        else:
            parStr = '    ' + parStr
            
        txt.append(parStr)
    return '\n'.join(txt)


if __name__ == "__main__":
    txt = read_docx()

新建一個 Word 測試文檔

爲了保證程序的正確運行,我們還需要在項目文件夾中,創建一個新的文件夾,命名爲 input。之後,再在裏面添加一個 .docx 文件,命名爲 demo.docx,輸入以下文字:
demo.docx 的內容

代碼調試

之後,我們可以運行一下 principle2.py 文件,用 debug 來觀察程序是否運行順暢。首先,在 return 處設置斷點,按 F11 進行調試,可以看到程序運行到斷點出,停止運行。並在 Varialbe 窗口出現一些變量(右邊)
設置斷點
我們可以直接在 Variable 窗口,點擊 txt,來觀察變量 txt 的取值。也可以在 command 窗口,輸入變量名 txt,來觀察變量的取值。結果如下:

[’ 謎題一個 ‘, ’ 鳥飛鵝跳,月上中梢,目上硃砂,已異非巳,勺旁傍白,萬事開頭,工戈不全,雨下摯友,稱斷人和。’, ’ 答案是:—— 我要用自己的方式來愛你!!!’]

之後,在按一下 F5(step into吧,好像是),再按一次(共兩次),就可以結束了。有關調試的內容,就不再過多說明了。

代碼運行準備

測試完畢,效果可嘉。爲了讓上述的 txt 變量讀入到 principle1.py 中,以供我們的程序使用,我們還要在 principle1.py 文件中,添加一些代碼,以保證程序的運行,如下:

......
from principle2 import *
#text = """
#鳥飛鵝跳,月上中梢,目上硃砂,已異非巳,勺旁傍白,萬事開頭,工戈不全,雨下摯友,稱斷人和
#"""
......
text = read_docx(r'./input/demo.docx')
......

再次運行 principle1.py,在 console 那裏,可以看到程序運行總時間爲 4.211 秒,不知道是字數多了,還是程序變複雜的緣故?想知道答案嗎?評論區提問一下吧!

來看一下輸出結果,在 output 文件夾中,打開圖像 0.jpg:
output文件夾0.jpg
課題看到,我們在 docx 文件中,第一行即標題行,並沒有用兩個回車,但是爲什麼會空了一行,我們的理想效果應該是這樣的:
理想效果
那麼,怎麼做呢?評論區回答或者提問一下吧!

GUI 設計

從代碼中,可以看出,我們需要的輸入變量一共有:

text:docx 文件路徑或者直接輸入
background:背景
fontsize:字體大小
font :字體
line_spacing : 行間距
fill : 字體顏色

當然,還有其他的。爲了簡化設計,我們將不考慮其他東西~

爲此,我們畫出 GUI 的草圖,如下所示:
草圖
草圖要有草圖的樣子,不要笑了[笑哭]。上面的草圖是頁面1,頁面2 我打算弄成一個預覽窗口。這個窗口能夠展示一個 demo,從而方便我們調整字體大小等參數。

整體代碼

我們再次新建一個項目,命名爲 Word2write,並在項目文件下面,新建一個 GUI 文件夾,再在文件夾下創建兩個 .py 文件: Tab1.py,Tab2.py。

GUI 骨架

至於具體實現,我們大致可以按照如下骨架來弄,如果覺得下圖很難看懂,那麼就乖乖看一下上面羅列的教程吧:
Tab1 的骨架
Tab2 骨架

文件準備

首先,我們的項目文件已經有 GUI 文件夾,此後還有 Tab1.py Tab2.py
之後,我們還需要創建一個 Fun 文件夾,用來保存一些邏輯功能。我們在下面創建兩個文件: fun1.py fun2.py
之後,還需要在項目文件中,創建 Input、Output、Font、Background 文件夾。
在 Backgroun 文件夾中放入背景圖片文件:background_01.jpg
並在項目文件中,放入 test.jpg 文件
文件夾

代碼

首先是 fun1.py

# coding: utf-8
from PIL import Image, ImageFont
import numpy as np
from handright import Template, handwrite
from multiprocessing import Pool
import time
from Fun.fun2 import *

def trans(input_path,output_path,font_path,line_spacing,if_test=False):
    background=Image.open(r'../Background/background_01.jpg')
    width, height = background.size
    background = background.resize((np.int(3*width),np.int(2.5*height)),resample=Image.LANCZOS)
    if not if_test:
        text = read_docx(input_path)
    else:
        text = """
    卿尋鯉影剔浮英, 
    我恨浮英掩玉卿。
     難教芳心知我心, 
     孤燭半影又天明。
    """
    time_start = time.time()
    template = Template(
        background=background,
        font_size=100,
        font=ImageFont.truetype(font_path),
        line_spacing=line_spacing,
        fill=0,  # ���塰��ɫ��
        left_margin=250,
        top_margin=-30,
        right_margin=100,
        bottom_margin=100,
        word_spacing=15,
        line_spacing_sigma=6,  # �м������Ŷ�
        font_size_sigma=1,  # �����С����Ŷ�
        word_spacing_sigma=3,  # �ּ������Ŷ�
        end_chars=",。;、“”",  # ��ֹ�ض��ַ����Ű��㷨���Զ����ж�����������
        perturb_x_sigma=3,  # �ʻ�����ƫ������Ŷ�
        perturb_y_sigma=3,  # �ʻ�����ƫ������Ŷ�
        perturb_theta_sigma=0.03,  # �ʻ���תƫ������Ŷ�
    )
    with Pool() as p:
    	images = handwrite(text, template,mapper=p.map)
    for i, im in enumerate(images):
        assert isinstance(im, Image.Image)
        if not if_test:
            im.save(output_path+"\{}.jpg".format(i))
        else:
            im.save(r"../test.jpg")
    time_end = time.time()
    print(time_end-time_start)

然後是 fun2.py

import docx
def read_docx(docx_path):
    docStr = docx.Document(docx_path)
    txt = []
    for para in docStr.paragraphs:
        parStr = para.text
        if_center = para.paragraph_format.alignment
        if if_center:
            parStr = parStr.center(30)
        else:
            parStr = '    ' + parStr
            
        txt.append(parStr)
    return '\n'.join(txt)


if __name__ == "__main__":
    txt = read_docx()

然後是 Tab1.py:

# -*- coding: utf-8 -*-

import tkinter as tk
from tkinter import ttk
from tkinter import messagebox as mBox
from tkinter import Menu
from sys import exit
from threading import Thread
from time import sleep
from queue import Queue
import os
from os import path
from tkinter import filedialog as fd
from Tab2 import Tab2
import sys
sys.path.append('../')
from PIL import Image, ImageFont
import numpy as np
from handright import Template, handwrite
from multiprocessing import Pool
import time
from Fun.fun1 import *
from PIL import ImageTk,Image

class OOP():
    def __init__(self):
        self.win = tk.Tk()
        self.win.title('妙筆生花')
        self.win.iconbitmap(r'./app.ico')
        self._createWidget()

    def _runTrans(self,input_path,output_path,font_path,line_spacing): 
        trans(input_path,output_path,font_path,line_spacing)
    def _testTrans(self,input_path,output_path,font_path,line_spacing): 
        trans(input_path,output_path,font_path,line_spacing,if_test=True)
        self.tab2Widget.createImage()
        self.tab2Widget._image = ImageTk.PhotoImage(self.tab2Widget.pil_image)
        self.tab2Widget.canvas.create_image(20,20,anchor='nw',image=self.tab2Widget._image)
        
    def createRunThread(self):
        input_path = str(self.inEntry.get())
        output_path = str(self.outEntry.get())
        font_path = str(self.fntEntry.get())  
        line_spacing = float(str(self.lineSpcEty.get())) 
        
        run = Thread(target=self._runTrans,args=(input_path,output_path,
                                                  font_path,line_spacing))    #創建一個線程,線程運行 methodInAThread
        run.setDaemon(True)    #將線程設置成守護線程
        run.start()
        
    def createTestThread(self):
        input_path = str(self.inEntry.get())
        output_path = str(self.outEntry.get())
        font_path = str(self.fntEntry.get())  
        line_spacing = float(str(self.lineSpcEty.get())) 
        
        test = Thread(target=self._testTrans,args=(input_path,output_path,
                                                  font_path,line_spacing))
        test.setDaemon(True)
        test.start()
        
    def _clickRunBut(self):
        self.createRunThread()
    
    def _clickTstBut(self):
        self.createTestThread()
         
    
    def _getFileName(self):
        fDir = os.path.join( os.path.dirname(__file__),'../..')  #���ϼ��ļ�Ŀ¼��
        fName = fd.askopenfilename(parent=self.inOuFrm,initialdir=fDir)
        fPath = path.dirname(fName)
        self.inEntry.delete(0,tk.END)   
        self.inEntry.insert(0,fName)   
    
    def _getFileName2(self):
        fDir = os.path.join( os.path.dirname(__file__),'..')  #���ϼ��ļ�Ŀ¼��
        fName = fd.askdirectory(parent=self.inOuFrm,initialdir=fDir)
        fPath = path.dirname(fName)
        self.outEntry.delete(0,tk.END)   
        self.outEntry.insert(0,fName)   
    
    def _getFileName3(self):
        fDir = os.path.join( os.path.dirname(__file__),'..')  #���ϼ��ļ�Ŀ¼��
        fName = fd.askopenfilename(parent=self.inOuFrm,initialdir=fDir)
        fPath = path.dirname(fName)
        self.fntEntry.delete(0,tk.END)   
        self.fntEntry.insert(0,fName) 
        
    

    def _quit(self):
        self.win.quit()
        self.win.destroy()
        exit()
            
    def _createWidget(self):
        self.menuBar = Menu(self.win)
        self.win.configure(menu=self.menuBar)

        self.startMenu = Menu(self.menuBar,tearoff=0)
        self.startMenu.add_command(label='保存爲')
        self.startMenu.add_separator()
        self.startMenu.add_command(label='退出',command=self._quit)
        self.menuBar.add_cascade(label='開始',menu=self.startMenu)
        
        self.helpMenu = Menu(self.menuBar,tearoff=0)
        self.helpMenu.add_command(label='幫助')
        self.helpMenu.add_command(label='關於')
        self.menuBar.add_cascade(label='其他',menu=self.helpMenu)
        
        
        self.tabControl = ttk.Notebook(self.win)
        self.tab1 = ttk.LabelFrame(self.tabControl)
        self.tab2 = ttk.LabelFrame(self.tabControl)
        self.tabControl.add(self.tab1,text='參數設置')
        self.tabControl.add(self.tab2,text='效果預覽')
        self.tabControl.pack(fill='both',expand=1)
        
        self.zhuo = ttk.Frame(self.tab1)
        self.zhuo.grid(column=0,row=0)
        
        self.inOuFrm = ttk.LabelFrame(self.zhuo,text='文件管理')
        self.inOuFrm.grid(column=0,row=0,sticky='W')
        
        self.inBut = ttk.Button(self.inOuFrm,text='輸入文件',
                               command=self._getFileName)
        self.inBut.grid(column=0,row=0)
        self.fDir = os.path.abspath(os.path.join( os.path.dirname(__file__),".."))

        self.default_value = tk.StringVar()
        self.default_value.set(self.fDir+'\Input\demo.docx')
        
        self.default_value2 = tk.StringVar()
        self.default_value2.set(self.fDir+'\Output')
        
        self.default_value3 = tk.StringVar()
        self.default_value3.set(self.fDir+'\Font\瘦金簡體.ttf')
        
        self.inEntry = ttk.Entry(self.inOuFrm,width=60,textvariable=self.default_value)
        self.inEntry.grid(column=1,row=0)
        self.inEntry.focus()


        
        
        self.outBut = ttk.Button(self.inOuFrm,text='輸出文件夾',
                               command=self._getFileName2)
        self.outBut.grid(column=0,row=1)
        self.outEntry = ttk.Entry(self.inOuFrm,width=60,textvariable=self.default_value2)
        self.outEntry.grid(column=1,row=1)
        
        self.fntBut = ttk.Button(self.inOuFrm,text='搜索字體',command=self._getFileName3)
        self.fntBut.grid(column=0,row=2)
        self.fntEntry = ttk.Entry(self.inOuFrm,width=60,textvariable=self.default_value3)
        self.fntEntry.grid(column=1,row=2)

        for child in self.inOuFrm.winfo_children():
            child.grid_configure(padx=10,pady=10,sticky='W')
        
        self.miniCon = tk.Frame(self.zhuo)
        self.miniCon.grid(column=0,row=1,sticky='W')
        ttk.Label(self.miniCon,text='字體大小').grid(column=0,row=0)
        self.fntSizeCom = ttk.Combobox(self.miniCon)
        self.fntSizeCom['value']=(80,90,100)
        self.fntSizeCom.grid(column=1,row=0)
        self.fntSizeCom.current(2)
        
        ttk.Label(self.miniCon,text='字體顏色').grid(column=2,row=0)
        self.fntColCom = ttk.Combobox(self.miniCon)
        self.fntColCom['value']=(0)
        self.fntColCom.grid(column=3,row=0,sticky='W')
        self.fntColCom.current(0)
        ttk.Label(self.miniCon,text='行間距').grid(column=0,row=1)
        self.default_value4 = tk.StringVar()
        self.default_value4.set(150)
        self.lineSpcEty = ttk.Entry(self.miniCon,textvariable=self.default_value4)
        self.lineSpcEty.grid(column=1,row=1)    
        
        
        self.runBut = ttk.Button(self.miniCon,text='轉換',command=self._clickRunBut)
        self.runBut.grid(column=0,row=2)
        
        self.testBut = ttk.Button(self.miniCon,text='測試',command=self._clickTstBut)
        self.testBut.grid(column=1,row=2)
        
        for child in self.miniCon.winfo_children():
            child.grid_configure(padx=10,pady=10,sticky='W')        
          


        """頁面2開發"""
        self.tab2Widget = Tab2(self.tab2)  
oop = OOP()
oop.win.mainloop()

然後是Tab2.py

# -*- coding: utf-8 -*-


import tkinter as tk
from tkinter import ttk

from PIL import ImageTk,Image
class Tab2():
    def __init__(self,tab):
        self._createWidget(tab)
    
        
    def _createWidget(self,tab):
        self.zhuo = tk.Frame(tab,bg='red')
        self.zhuo.grid(column=0,row=0)
        self.canvas = tk.Canvas(self.zhuo)
        self.createImage()
        self.image = ImageTk.PhotoImage(self.pil_image)    #這裏就不是 tk.PhotoImage 了
        self.canvas.create_image(20,20,anchor='nw',image=self.image)    #打開圖像
        self.canvas.grid(row=0,column=0,columnspan=2)
        
    def _resize(self,w, h, w_box, h_box, pil_image):  
        f1 = 1.0*w_box/w 
        f2 = 1.0*h_box/h  
        factor = min([f1, f2])  
        width = int(w*factor)  
        height = int(h*factor)  
        self.pil_image = pil_image.resize((width, height), Image.ANTIALIAS) 

    
    def createImage(self):        
        pil_image = Image.open(r'../test.jpg')    #記得在縮放圖像之前,要用 Image 模塊打開。
        w, h = pil_image.size
        w_box = 400   #大夥可以調節這個,來設置圖片的大小。當然,也建議讀者們把他設爲 公有的。
        h_box = 500
        self._resize(w,h,w_box,h_box,pil_image)

運行 Tab1.py,再點擊運行按鈕時,會出現多個彈窗的情況,如下所示:
在這裏插入圖片描述
而我們要的效果是,點擊後不能出現這樣的彈窗。原因在哪裏呢?,請到評論區提問或回答,我會回覆的哦。

軟件的使用與後續開發

使用效果

解決完上述彈窗問題之後,我們就來驗證一下我們的軟件吧:

首先運行 Tab1.py,就會彈出軟件窗口,再點擊測試,並在效果預覽頁面觀察效果圖,如下所示:
在這裏插入圖片描述
也可以修改字體,點擊搜索字體,選擇李國夫手寫體,再點測試:
在這裏插入圖片描述
在這裏插入圖片描述
在 Input 中新建一個 demo.docx 文件,並任意輸入內容,我這裏輸入的是:
在這裏插入圖片描述
然後,點擊轉換,就可以在 Input 文件夾中,生成一個 0.jpg 的文件,就可以啦:
在這裏插入圖片描述

後續完善

其實還有很多要完善的地方。比如運行的時候,顯示進度條。而且還要提高運行效率。除此之外,一些參數的設置要更加完善。如果大家喜歡這個軟件的話,可以關注我。我把 Github 源碼鏈接發給你們,咱們一起開發和完善。

另外,還要轉換成 exe 文件,這裏就不再展示了。大家可以參考上面羅列的教程。

爲了提高效率、以及實用性,我們還需要用 Java 來開發一個一模一樣的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章