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 来开发一个一模一样的。

猜你喜欢

转载自blog.csdn.net/weixin_42141390/article/details/106840073
今日推荐