用于pdf翻译的PdfTranlate

  项目地址:https://gitee.com/Shanyalin/pdf-tranlate  

  关于pdf翻译,有以下几个需要注意的点。

  1.文本提取。从pdf中提取文本用于翻译,在我们不清楚pdf格式的情况下,我们不能想当然的认为pdf的格式都是一样的,页面上一行行从上到下提取就可以。有不少的文档分左右两栏,甚至左中右三栏。如果按行提取,极有可能上下文语义不通,后续的翻译就不必再提。因此文本提取的关键在于符合人们阅读习惯的情况下,尽可能的按段落进行提取。

  2.翻译。翻译不算是pdf翻译的核心要点,现在通用的免费的翻译接口很多,仅百度的也能满足大多数应用场景。

  3.输出pdf。翻译后的文本应该怎样输出,其实有多个方案。如不按照原来的格式,自定义规则输出;如双语排版,一段原文一段译文;或者按照原文档格式原样输出。自定义规则和双语排版更适用于不分栏且没有图表的纯文本。考虑到pdf格式不统一,按照原样式输出相对更简单一些。

  经过两周左右的调研,符合阅读习惯的文本提取更加便于文本的整合。C#和java项目或库,或完全开源或半开源,在某些关键性问题上有疑难。鉴于pdf翻译的第一个要点,最终选择了py的pymupdf作为操作pdf的库。结合QPromise/EasyTrans项目的启发,写了这个脚本,既可以集成在EasyTrans里替换原来翻译,也可以单独以脚本的形式来进行pdf翻译。EasyTrans是基于django的网站翻译系统,内部集成了谷歌、百度、搜狗、有道翻译接口,主要做英译中的翻译;也可以上传pdf文件翻译。

  以下简单列表比较以下调研的库或项目的特点,以便后期扩展。由于时间问题,调研并不充分,有些功能的使用和描述并不准确,仅就调试中出现的问题进行讨论。

项目名称/库 语言 调研功能 结果分析
spire.pdf C#/java

文本提取:可以提取指定区域,可以整页提取,无法处理表格

图片提取:正常

转html:标签以字符为单位,需自行开发重新组合的方法

提取结果不符合阅读习惯。

根据图片切分页面,提取指定区域文本,可能会在没有图片时整页提取

Aspose

C#/java

文本提取速度极慢,官方文档提取demo多次有异常

文本提取出现无法识别的字符

Itextsharp

C#/java

提供了整页提取和范围提取的api,整页提取时出现乱码现象

乱码现象短期不能解决,且本库常用于写pdf

Npoi

C#

Windows office可以将pdf转为docx,再通过nopi来提取

需前置Windows office,且转换过程中有可能丢失文件信息

QPromise/EasyTrans

Py

适用于译文比原文短的情况,无法处理表格

文本提取符合阅读习惯。

图片提取保存时不支持某些格式。

Pdf_translator

py

先将pdf转换为图片,对图片进行ocr识别,记录位置信息来保持原有格式

引入Ocr识别增加了额外的风险

Pdfpig

C#

BobLd/pdfDocumentLayoutAnalysis,

BobLd/simple-docstrum。

项目提供了6种组合的算法,符合人们的阅读习惯

文本识别有问题,有中文变韩语乱码的现象

  通过对EasyTrans项目的调试,发现直接使用pymupdf的get_text_blocks()方法提取出来的blocks还是不能满足我们的需要,因此重新对blocks进行计算重排。

  主要的思路是逐页遍历blocks,比较当前的block与上一个block的位置关系。Block提供了对应的座标(分别是左上角和右下角),我们也可以实例化对应的rect,根据block的x0、y1,我们可以确定文本是否有缩进及行间距大小,判断两个block是否是同一段落。将属于同一段的block进行合并,把页面的blocks重新进行组合。通过阅读文档和调试,发现将文本提取成字典dict,对字典中的blocks进行自定义整合更能满足我们的需求。部分代码如下,完整代码请阅读项目文件。

def dicts2blocks(extract_dict):
    blocks = extract_dict['blocks']
    blks = []
    for d in blocks:
        bbox, type, fz, text = d['bbox'], d['type'], 0, ''
        if type == 0:  # 文本
            lines = d['lines']
            for l in lines:
                for s in l['spans']:
                    fz = max(fz, s['size'])
                    text += s['text']
                pass
        else:  # 非文本
            pass
        blks.append((bbox[0], bbox[1], bbox[2], bbox[3], text + '\n', fz, type))
    return blks
def rebulid_blocks(blks: list, is_height_in_block=False):
    re_blks = []
    rect_curr, rect_pre, text_pre, type_pre = None, None, '', 0
    width_curr, width_pre, height_curr, height_pre = 0, 0, 0, 0
    for i, b in enumerate(blks):
        if i == 0:
            rect_pre, text_pre, type_pre = fitz.Rect(b[:4]), b[4], b[-1]
            try:
                index_pre = text_pre.index('\n')  # 首行非文本时异常
            except Exception as ex:
                index_pre = -1
            width_pre, height_pre = b[-2] if is_height_in_block else rect_pre.width / max(index_pre + 2, len(text_pre)), \
                                    b[-2] if is_height_in_block else rect_pre.height / (
                                            text_pre.strip('\n').count('\n') + 1)
            continue
        rect_curr, text_curr, type_curr = fitz.Rect(b[:4]), b[4], b[-1]
        if type_curr != 0:  # 当前非文本
            re_blks.append((rect_pre, text_pre, type_pre))
            rect_pre, text_pre, type_pre, width_pre, height_pre = rect_curr, text_curr, type_curr, width_curr, height_curr
        else:  # 当前文本
            try:
                index_curr = text_curr.index('\n')
            except Exception as ex:
                index_curr = -1
            width_curr, height_curr = b[-2] if is_height_in_block else rect_curr.width / max(index_curr + 2,
                                                                                             len(text_curr)), b[
                                          -2] if is_height_in_block else rect_curr.height / (
                    text_curr.strip('\n').count('\n') + 1)
            if 0 <= rect_curr.y1 - rect_pre.y1 <= 1.8 * height_curr \
                    and -0.2 * width_curr <= rect_pre.x0 - rect_curr.x0 <= 4 * width_curr \
                    and rect_curr.y0 >= rect_pre.y1:  # 同一段洛
                # 与上一行属于同一段
                # 右下角纵座标差距为1.8个字符宽度(段落首行字符宽度) 左上角纵座标差距-0.2-4个字符高度(段落首行字符高度) 当前左上纵座标应大于上一行右下纵座标
                # 右下角纵座标差距为1.8个字符宽度  左上角纵座标差距-0.2-4个字符高度  当前左上纵座标应大于上一行右下纵座标
                
                rect_pre = fitz.Rect(min(rect_pre.x0, rect_curr.x0), min(rect_pre.y0, rect_curr.y0),
                                     max(rect_pre.x1, rect_curr.x1), max(rect_pre.y1, rect_curr.y1))
                text_pre += text_curr
                type_pre = type_pre
                # 不修改width_pre 和 height_pre
                pass
            else:
                re_blks.append((rect_pre, text_pre, type_pre))
                rect_pre, text_pre, type_pre, width_pre, height_pre = rect_curr, text_curr, type_curr, width_curr, height_curr
    re_blks.append((rect_pre, text_pre, type_pre))
    return re_blks
dicts = cur_page.get_text('dict')
blks = dicts2blocks(dicts)
blks = rebulid_blocks(blks, True)

 

  翻译,可使用百度接口,具体不做赘述。

  我选择原样输出pdf的方式,所以在遍历重组后的rebulid_blocks时,对每个block中的文本进行翻译,然后将翻译结果进行回填即可。EasyTrans项目中使用的是insert_textbox在指定区域输出文本,需要控制字体的大小,注意此方法是有返回值的,结果大于0时表示写入成功。

  经调试发现,如果是中译英,译文较长的情况,指定固定的字体大小的话,大概率不能输出成功。因此使用insert_textbox之前最好先计算出字体的大小。默认的字体不支持中文的输出,easytrans提供了宋体的输出方法及字体文件。通过阅读insert_textbox源码,发现内部已经实现了关于字体大小的计算,但是源码中只针对指定的字体大小进行计算,对相关代码提取改造之后,即可获得在指定范围内输入指定行高指定内容的最大字号的方法。这样做的好处是页面看起来比较充实,不至于有大面积留白(字号偏大,输出失败时rect内为空白;字号偏小,输出成功时rect内有大量空白没有使用);坏处在于段落间字号的大小不一致。当然也有一些方案进行改进,但结果一定是部分段落留白较多。

def calc_fontsize(rect, content, lang='zh', lineheight=1.5):
    rect = fitz.Rect(rect)
    fz = 12.0
    font = fitz.Font(fontname='song', fontfile='resource/SimSun.ttf') if lang == 'zh' else fitz.Font('helv')
    maxwidth, maxpos = rect.width, rect.y1
    for s in range(130, 10, -3):
        s = float(s / 10)
        point = rect.tl + fitz.Point(0, s * font.ascender)
        pos = point.y  # 换行时发生变化
        lbuff, rest = '', maxwidth
        line_t = content.expandtabs(1).split(' ')
        lheight = s * lineheight
        # text,  just_tab = '',  []
        for word in line_t:
            pl_w = font.text_length(word, fontsize=s)
            if rest >= pl_w:  # 当前行剩余空间可以容纳当前文本
                lbuff += word + ' '
                rest -= pl_w + s
                continue
            if len(lbuff) > 0:  # 当前行剩余空间不能容纳当前字符,且当前行内已有文本,需换行
                lbuff = lbuff.rstrip() + '\n'
                pos += lheight
                # text += lbuff
                # just_tab.append(True)
                lbuff = ''
            rest = maxwidth  # 换行 重新计算剩余空间
            if pl_w <= maxwidth:  # 完整空行可以容纳当前字符
                lbuff = word + ' '
                rest = maxwidth - pl_w - s
                continue
            # 换行后完整空行不能容纳当前字符串
            # if len(just_tab) > 0:
            # just_tab[-1] = False  # 标记非正常断行
            for c in word:
                if font.text_length(lbuff, fontsize=s) <= maxwidth - font.text_length(c, fontsize=s):  # buff中可以容纳当前字符
                    lbuff += c
                else:  # 当前行满了
                    lbuff += '\n'
                    pos += lheight
                    # text += lbuff
                    # just_tab.append(False)
                    lbuff = c
            lbuff += ' '
            rest = maxwidth - font.text_length(lbuff, fontsize=s)

        if lbuff != '':  # 最后一行有剩余文本
            # text += lbuff.rstrip()
            # just_tab.append(False)
            pass

        more = pos - maxpos
        if more > fitz.utils.EPSILON:  # 超出范围
            continue
        fz = s
        break
    return fz
    pass

 

  至此,pdf从提取到翻译,再到输出就已经初步结束了。剩下的就是根据需求细化了。

  

 

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