提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
首先介绍一下需求,我是突然接到的财务部门的需求,需要自动合成电子发票,利用两个pdf,一个是固定的发票模板,一个是财务平台导出的发票内容文件,合成最终的电子发票。
:
# pdf转为png写入的文件夹创建(先存png然后再读png)
png_dir = os.path.join(os.path.split(text_p)[0], 'png')
if not os.path.exists(png_dir):
os.mkdir(png_dir)
out_dir = os.path.join(os.path.split(text_p)[0], '开票完成')
if not os.path.exists(out_dir):
os.mkdir(out_dir)
# 创建结果图和结果pdf的存储路径
res_idx = i
res_img_p = os.path.join(out_dir, str(res_idx) + '.jpg')
res_pdf_p = os.path.join(out_dir, str(res_idx) + '.pdf')
# 打开模板和文字的pdf文件
forms = fitz.open(form_p)
texts = fitz.open(text_p)
2.pdf转存为png并读取
for pg in range(0, 1):
form = forms[pg]
text = texts[pg]
# 模板和文字的放大倍率
zoom = int(400)
zoom1 = int(400)
# 模板和文字的旋转角度
rotate = int(270)
rotate1 = int(0)
# 表格
trans = fitz.Matrix(zoom / 100.0, zoom / 100.0).preRotate(rotate)
pm = form.getPixmap(matrix=trans, alpha=0)
# 文字
trans1 = fitz.Matrix(zoom1 / 100.0, zoom1 / 100.0).preRotate(rotate1)
pm1 = text.getPixmap(matrix=trans1, alpha=0)
form_png_p = os.path.join(png_dir, 'form' + str(pg + 1) + '.png')
text_png_p = os.path.join(png_dir, 'text' + str(pg + 1) + '.png')
pm.writePNG(form_png_p)
pm1.writePNG(text_png_p)
"""读取png"""
# im_form = Image.fromarray(Image.open(form_png_p))
# im_text = Image.fromarray(Image.open(text_png_p))
# im_form_bgr = cv2.cvtColor(im_form, cv2.COLOR_RGB2BGR)
# im_text_bgr = cv2.cvtColor(im_text, cv2.COLOR_RGB2BGR)
im_form_bgr = cv2.imdecode(np.fromfile(form_png_p, np.uint8), -1)
im_text_bgr = cv2.imdecode(np.fromfile(text_png_p, np.uint8), -1)
im_form_gray = cv2.cvtColor(im_form_bgr, cv2.COLOR_BGR2GRAY)
im_text_gray = cv2.cvtColor(im_text_bgr, cv2.COLOR_BGR2GRAY)
代码如下(示例):
3.ocr及发票合成部分
创建一张和发票模板一样大小的空白图,然后将文字图放在正中间。
_, thre_form = cv2.threshold(im_form_gray, 240, 255, cv2.THRESH_BINARY)
_, thre_text = cv2.threshold(im_text_gray, 240, 255, cv2.THRESH_BINARY)
thre_x_form, thre_y_form = np.where(thre_form <= 10)
thre_x_text, thre_y_text = np.where(thre_text <= 10)
x_min_form, x_max_form, y_min_form, y_max_form = np.min(thre_x_form), np.max(thre_x_form), np.min(thre_y_form), np.max(thre_y_form)
x_min_text, x_max_text, y_min_text, y_max_text = np.min(thre_x_text), np.max(thre_x_text), np.min(thre_y_text), np.max(thre_y_text)
w_form, h_form = x_max_form - x_min_form, y_max_form - y_min_form
w_text, h_text = x_max_text - x_min_text, y_max_text - y_min_text
form_roi_bgr = im_form_bgr[x_min_form: x_max_form, y_min_form: y_max_form]
text_roi_bgr = im_text_bgr[x_min_text: x_max_text, y_min_text: y_max_text]
w_f, h_f, c_f = form_roi_bgr.shape
w_t, h_t, c_t = text_roi_bgr.shape
# cv2.imshow('text', cv2.resize(im_text_bgr, (0,0), fx=0.5, fy=0.5))
# cv2.imshow('text roi', cv2.resize(text_roi_bgr, (0,0), fx=0.5, fy=0.5))
x_start1, y_start1 = int((w_f - w_t) * 0.43), int((h_f - h_t) * 0.47)
x_end1, y_end1 = x_start1 + int(w_t * 0.73), y_start1 + h_t
x_start2, y_start2 = x_end1 + int(w_t * 0.03), y_start1
x_end2, y_end2 = x_start2 - 1 + w_t - int(w_t * 0.73), y_start2 + h_t
# 按照模板大小新建空白背景图,在中间放上文字图
text_roi_new_bgr = np.ones(form_roi_bgr.shape, dtype=np.uint8) * 255
text_roi_new_bgr[x_start1:x_end1, y_start1:y_end1] = text_roi_bgr[:int(w_t * 0.73), :]
text_roi_new_bgr[x_start2:x_end2, y_start2:y_end2] = text_roi_bgr[(int(w_t * 0.73) + 1):, :]
由于每个发票的两个编号都不同,但是我们只有一个,模板,所以要将发票两个编号的位置涂白,然后把文字内容中的两个编号内容识别出来写入模板里面,代码如下:
code1_img = text_roi_bgr[6:54, 1978:]
code2_img = text_roi_bgr[43:108, 1978:]
# cv2.imshow('code1', code1_img)
# cv2.imshow('code2', code2_img)
# cv2.waitKey(0)
"""先把模板的两个编号涂白"""
form_roi_bgr[120:210, 510:1050] = [255,255,255]
form_roi_bgr[120:210, 2010:2300] = [255,255,255]
"""识别两个编号"""
code1_str = ocr.ocr(code1_img, cls=True)[-1][-1][0]
code2_str = ocr.ocr(code2_img, cls=False)[-1][-1][0]
allstr = ocr.ocr(text_roi_bgr)
print(code1_str[0], code2_str[0])
# code1_str = "abv"
# code2_str = "abc"
"""模板的两个编号替换掉"""
setFont = ImageFont.truetype('C:\\Windows\\Fonts\\simfang.ttf', size=80)
# text = "abc"
# fillcolor = '#ff0000'
# fillcolor = (101,101,101)
fillcolor = (76,80,79)
setFont1 = ImageFont.truetype('C:\\Windows\\Fonts\\msmincho.ttc', size=70)
# text1 = "abc"
# fillcolor1 = '#ff0000'
# fillcolor1 = (149,148,190)
fillcolor1 = (110,114,205)
form_roi_rgb = Image.fromarray(np.array(cv2.cvtColor(form_roi_bgr, cv2.COLOR_BGR2RGB), np.uint8))
# form_roi_rgb = Image.fromarray(form_roi_rgb)
text_roi_new_gray = cv2.cvtColor(text_roi_new_bgr, cv2.COLOR_BGR2GRAY)
text_roi_new_rgb = cv2.cvtColor(text_roi_new_bgr, cv2.COLOR_BGR2RGB)
draw = ImageDraw.Draw(form_roi_rgb)
draw.text((520,130), text=code1_str[0], font=setFont, fill=fillcolor)
draw.text((565, 130), text=code1_str[1], font=setFont, fill=fillcolor)
draw.text((610,130), text=code1_str[2], font=setFont, fill=fillcolor)
draw.text((655, 130), text=code1_str[3], font=setFont, fill=fillcolor)
draw.text((700,130), text=code1_str[4], font=setFont, fill=fillcolor)
draw.text((745, 130), text=code1_str[5], font=setFont, fill=fillcolor)
draw.text((790,130), text=code1_str[6], font=setFont, fill=fillcolor)
draw.text((835, 130), text=code1_str[7], font=setFont, fill=fillcolor)
draw.text((880,130), text=code1_str[8], font=setFont, fill=fillcolor)
draw.text((925, 130), text=code1_str[9], font=setFont, fill=fillcolor)
draw.text((970,130), text=code1_str[10], font=setFont, fill=fillcolor)
draw.text((1015, 130), text=code1_str[11], font=setFont, fill=fillcolor)
draw.text((2010,130), text=code2_str, font=setFont1, fill=fillcolor1)
将发票其余文字内同直接剪切到模板中:
form_roi_rgb = np.array(form_roi_rgb)
# form_roi_rgb[text_roi_new_rgb <= 240] = text_roi_new_rgb[text_roi_new_rgb <= 240]
form_roi_rgb[text_roi_new_gray <= 240] = [119, 118, 122]
电子发票结果图存为pdf
res_img = form_roi_rgb
# code1_img = res_img[120:130, 540:600]
# code1_img_big = Image.fromarray(code1_img).resize((420, 70))
# res_img[120:190, 540:960] = np.array(code1_img_big)
Image.fromarray(code1_img).show()
# code1_img_big.show()
Image.fromarray(res_img).show()
Image.fromarray(res_img).save(res_img_p)
doc = fitz.open()
res_im_doc = fitz.open(res_img_p)
pdfbytes = res_im_doc.convertToPDF()
res_im_pdf = fitz.open('pdf', pdfbytes)
doc.insertPDF(res_im_pdf)
doc.save(res_pdf_p)
doc.close()
该处使用的url网络请求的数据。
4.结果展示
下图就是我们的代码自动合成的电子发票,两个编号都是ocr识别后写上去的。右边的蓝色编号由于字体暂时没有换,所以和发票模板上的字体略有不同,后续会考虑换掉。ps:应该是哥特印刷体,CSDN上有字体文件下载(需付费)
5.全部代码
from PIL import Image, ImageDraw, ImageFont
import numpy as np
import numpy as np
from pdf2image import convert_from_bytes, convert_from_path
import fitz
import tempfile
from wand.image import Image as wi
import glob
from paddleocr import PaddleOCR, draw_ocr
import os
import cv2
def read_pdf(form_p, text_p, i, ocr):
# pdf转为png写入的文件夹创建(先存png然后再读png)
png_dir = os.path.join(os.path.split(text_p)[0], 'png')
if not os.path.exists(png_dir):
os.mkdir(png_dir)
out_dir = os.path.join(os.path.split(text_p)[0], '开票完成')
if not os.path.exists(out_dir):
os.mkdir(out_dir)
# 创建结果图和结果pdf的存储路径
res_idx = i
res_img_p = os.path.join(out_dir, str(res_idx) + '.jpg')
res_pdf_p = os.path.join(out_dir, str(res_idx) + '.pdf')
# 打开模板和文字的pdf文件
forms = fitz.open(form_p)
texts = fitz.open(text_p)
for pg in range(0, 1):
form = forms[pg]
text = texts[pg]
# 模板和文字的放大倍率
zoom = int(400)
zoom1 = int(400)
# 模板和文字的旋转角度
rotate = int(270)
rotate1 = int(0)
# 表格
trans = fitz.Matrix(zoom / 100.0, zoom / 100.0).preRotate(rotate)
pm = form.getPixmap(matrix=trans, alpha=0)
# 文字
trans1 = fitz.Matrix(zoom1 / 100.0, zoom1 / 100.0).preRotate(rotate1)
pm1 = text.getPixmap(matrix=trans1, alpha=0)
form_png_p = os.path.join(png_dir, 'form' + str(pg + 1) + '.png')
text_png_p = os.path.join(png_dir, 'text' + str(pg + 1) + '.png')
pm.writePNG(form_png_p)
pm1.writePNG(text_png_p)
"""读取png"""
# im_form = Image.fromarray(Image.open(form_png_p))
# im_text = Image.fromarray(Image.open(text_png_p))
# im_form_bgr = cv2.cvtColor(im_form, cv2.COLOR_RGB2BGR)
# im_text_bgr = cv2.cvtColor(im_text, cv2.COLOR_RGB2BGR)
im_form_bgr = cv2.imdecode(np.fromfile(form_png_p, np.uint8), -1)
im_text_bgr = cv2.imdecode(np.fromfile(text_png_p, np.uint8), -1)
im_form_gray = cv2.cvtColor(im_form_bgr, cv2.COLOR_BGR2GRAY)
im_text_gray = cv2.cvtColor(im_text_bgr, cv2.COLOR_BGR2GRAY)
_, thre_form = cv2.threshold(im_form_gray, 240, 255, cv2.THRESH_BINARY)
_, thre_text = cv2.threshold(im_text_gray, 240, 255, cv2.THRESH_BINARY)
thre_x_form, thre_y_form = np.where(thre_form <= 10)
thre_x_text, thre_y_text = np.where(thre_text <= 10)
x_min_form, x_max_form, y_min_form, y_max_form = np.min(thre_x_form), np.max(thre_x_form), np.min(thre_y_form), np.max(thre_y_form)
x_min_text, x_max_text, y_min_text, y_max_text = np.min(thre_x_text), np.max(thre_x_text), np.min(thre_y_text), np.max(thre_y_text)
w_form, h_form = x_max_form - x_min_form, y_max_form - y_min_form
w_text, h_text = x_max_text - x_min_text, y_max_text - y_min_text
form_roi_bgr = im_form_bgr[x_min_form: x_max_form, y_min_form: y_max_form]
text_roi_bgr = im_text_bgr[x_min_text: x_max_text, y_min_text: y_max_text]
w_f, h_f, c_f = form_roi_bgr.shape
w_t, h_t, c_t = text_roi_bgr.shape
# cv2.imshow('text', cv2.resize(im_text_bgr, (0,0), fx=0.5, fy=0.5))
# cv2.imshow('text roi', cv2.resize(text_roi_bgr, (0,0), fx=0.5, fy=0.5))
x_start1, y_start1 = int((w_f - w_t) * 0.43), int((h_f - h_t) * 0.47)
x_end1, y_end1 = x_start1 + int(w_t * 0.73), y_start1 + h_t
x_start2, y_start2 = x_end1 + int(w_t * 0.03), y_start1
x_end2, y_end2 = x_start2 - 1 + w_t - int(w_t * 0.73), y_start2 + h_t
# 按照模板大小新建空白背景图,在中间放上文字图
text_roi_new_bgr = np.ones(form_roi_bgr.shape, dtype=np.uint8) * 255
text_roi_new_bgr[x_start1:x_end1, y_start1:y_end1] = text_roi_bgr[:int(w_t * 0.73), :]
text_roi_new_bgr[x_start2:x_end2, y_start2:y_end2] = text_roi_bgr[(int(w_t * 0.73) + 1):, :]
# cv2.imshow('text', text_roi_bgr)
# cv2.imshow('tmp', cv2.resize(text_roi_bgr[:int(w_t * 0.73), :], (0,0), fx=0.5, fy=0.5))
# cv2.imshow('tmp1', cv2.resize(text_roi_bgr[(int(w_t * 0.73) + 1):, :], (0,0), fx=0.5, fy=0.5))
# cv2.waitKey(0)
cv2.imencode('.jpg', text_roi_bgr)[-1].tofile(r'D:\Code\Code\实用小脚本\发票合成20210302\123\test\开票完成\tmp.jpg')
code1_img = text_roi_bgr[6:54, 1978:]
code2_img = text_roi_bgr[43:108, 1978:]
# cv2.imshow('code1', code1_img)
# cv2.imshow('code2', code2_img)
# cv2.waitKey(0)
"""先把模板的两个编号涂白"""
form_roi_bgr[120:210, 510:1050] = [255,255,255]
form_roi_bgr[120:210, 2010:2300] = [255,255,255]
"""识别两个编号"""
code1_str = ocr.ocr(code1_img, cls=True)[-1][-1][0]
code2_str = ocr.ocr(code2_img, cls=False)[-1][-1][0]
allstr = ocr.ocr(text_roi_bgr)
print(code1_str[0], code2_str[0])
# code1_str = "abv"
# code2_str = "abc"
"""模板的两个编号替换掉"""
setFont = ImageFont.truetype('C:\\Windows\\Fonts\\simfang.ttf', size=80)
# text = "abc"
# fillcolor = '#ff0000'
# fillcolor = (101,101,101)
fillcolor = (76,80,79)
setFont1 = ImageFont.truetype('C:\\Windows\\Fonts\\msmincho.ttc', size=70)
# text1 = "abc"
# fillcolor1 = '#ff0000'
# fillcolor1 = (149,148,190)
fillcolor1 = (110,114,205)
form_roi_rgb = Image.fromarray(np.array(cv2.cvtColor(form_roi_bgr, cv2.COLOR_BGR2RGB), np.uint8))
# form_roi_rgb = Image.fromarray(form_roi_rgb)
text_roi_new_gray = cv2.cvtColor(text_roi_new_bgr, cv2.COLOR_BGR2GRAY)
text_roi_new_rgb = cv2.cvtColor(text_roi_new_bgr, cv2.COLOR_BGR2RGB)
draw = ImageDraw.Draw(form_roi_rgb)
draw.text((520,130), text=code1_str[0], font=setFont, fill=fillcolor)
draw.text((565, 130), text=code1_str[1], font=setFont, fill=fillcolor)
draw.text((610,130), text=code1_str[2], font=setFont, fill=fillcolor)
draw.text((655, 130), text=code1_str[3], font=setFont, fill=fillcolor)
draw.text((700,130), text=code1_str[4], font=setFont, fill=fillcolor)
draw.text((745, 130), text=code1_str[5], font=setFont, fill=fillcolor)
draw.text((790,130), text=code1_str[6], font=setFont, fill=fillcolor)
draw.text((835, 130), text=code1_str[7], font=setFont, fill=fillcolor)
draw.text((880,130), text=code1_str[8], font=setFont, fill=fillcolor)
draw.text((925, 130), text=code1_str[9], font=setFont, fill=fillcolor)
draw.text((970,130), text=code1_str[10], font=setFont, fill=fillcolor)
draw.text((1015, 130), text=code1_str[11], font=setFont, fill=fillcolor)
draw.text((2010,130), text=code2_str, font=setFont1, fill=fillcolor1)
form_roi_rgb = np.array(form_roi_rgb)
# form_roi_rgb[text_roi_new_rgb <= 240] = text_roi_new_rgb[text_roi_new_rgb <= 240]
form_roi_rgb[text_roi_new_gray <= 240] = [119, 118, 122]
res_img = form_roi_rgb
# code1_img = res_img[120:130, 540:600]
# code1_img_big = Image.fromarray(code1_img).resize((420, 70))
# res_img[120:190, 540:960] = np.array(code1_img_big)
Image.fromarray(code1_img).show()
# code1_img_big.show()
Image.fromarray(res_img).show()
Image.fromarray(res_img).save(res_img_p)
doc = fitz.open()
res_im_doc = fitz.open(res_img_p)
pdfbytes = res_im_doc.convertToPDF()
res_im_pdf = fitz.open('pdf', pdfbytes)
doc.insertPDF(res_im_pdf)
doc.save(res_pdf_p)
doc.close()
form_p = r"1.纸质发票空白.pdf"
text_dir = r'D:\Code\Code\实用小脚本\发票合成20210302\123\test'
ocr = PaddleOCR(use_angle_cls = True, use_gpu = False)
for i, text_p in enumerate(glob.glob(os.path.join(text_dir, '*f'))):
read_pdf(form_p, text_p, i, ocr)
总结
本文展示了如何利用ocr及图像处理技术批量自动合成电子发票。目前只是个小demo,后续会利用pyqt做成软件,敬请期待。