OCR电子发票批量自动合成软件

本文介绍了一种使用OCR技术和图像处理批量自动合成电子发票的方法。通过读取PDF文件、图像处理和文字识别,实现电子发票的自动化批量制作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

首先介绍一下需求,我是突然接到的财务部门的需求,需要自动合成电子发票,利用两个pdf,一个是固定的发票模板,一个是财务平台导出的发票内容文件,合成最终的电子发票。

发票模板
电子发票文字内容

![电子发票内容](https://img-blog.csdnimg.cn/20210310101938328.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwMTQ3ODg4,size_16,color_FFFFFF,t_70下图为合成后的电子发票
合成后的电子发票


一、程序语言和库

语言为python
需要用到的库有Opencv, PIL, fitz, paddleocr, numpy, os, glob
图像处理部分用cv2
OCR部分用paddleocr
pdf解析和保存以及图片pdf互相转换用fitz
模板上写文字用PIL

示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。

二、代码解析

1.打开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做成软件,敬请期待。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值