用於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從提取到翻譯,再到輸出就已經初步結束了。剩下的就是根據需求細化了。

  

 

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