• 用于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 + '
    ', 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('
    ')  # 首行非文本时异常
                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('
    ').count('
    ') + 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('
    ')
                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('
    ').count('
    ') + 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() + '
    '
                    pos += lheight
                    # text += lbuff
                    # just_tab.append(True)
                    lbuff = ''
                rest = maxwidth  # 换行 重新计算剩余空间
                if pl_w <= max  # 完整空行可以容纳当前字符
                    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 += '
    '
                        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从提取到翻译,再到输出就已经初步结束了。剩下的就是根据需求细化了。

      

  • 相关阅读:
    Hz赫兹的定义
    NetCore 迅速接入微信支付+支付宝支付 payLink C# 交差并集
    C# 生产随机数 --几乎可以做到不重复
    Spark MLib完整基础入门教程
    (转)Scrapy 教程
    (转)python自顶向下设计步骤_python实现自顶向下,自底向上
    (转)scrapy架构图与执行流程
    (转)Python:字典(zip, dict)
    (转)APUE第13章 守护进程Deameon
    (转)Python开发指南
  • 原文地址:https://www.cnblogs.com/cnDqf/p/15006859.html
Copyright © 2020-2023  润新知