一.常见的PDF文档翻译方案:
1. 以 Word 文档为中间格式的翻译方案 ( DeepL 翻译 )
该方案先将 PDF 转换为 Word 格式,然后翻译并替换 Word 中的文字,最后利用 Word 的自动排版功能,重新生成翻译后的 PDF。
缺点: 由于 PDF 格式的灵活性,将 PDF 转换为 Word 不能完美地保留所有格式。同时,翻译后的自动排版也可能导致一些表格错乱。
2. 直接翻译并在原位置替换 PDF 的文本( Google 翻译 )
该方案通过提取 PDF 文本的详细信息(文字、颜色、字体、位置、反向等),然后在原来的位置进行翻译并替换原文,以达到翻译的效果。
缺点: 当译文比原文更长时,需要将字体缩小,才能放在原位置。
然而,在英译中的情况下,这个缺点大多数情况下并不存在。例如:
原文: PDF文档因为格式过于灵活导致难以翻译.
译文: PDF documents are difficult to translate due to their highly flexible format.
OTranslator.com 使用方案2的实现, 使用ChatGPT翻译替代Google的机器翻译, 同时针对PDF格式的多种情况进行优化。
具体效果可以之前文章中的介绍。
如何使用ChatGPT翻译文档(PDF/EPUB/Word/Excel/PowerPoint)?
二.如何使用PDF.js提取PDF中的文本信息?
PDF.js 是一个JavaScript库,用于在网页上渲染和交互显示PDF文档。虽然PDF.js主要用于在浏览器中呈现PDF文档,但它也提供了一些API和功能,可以用于从PDF中提取文本信息。
1. 安装pdf.js
npm install pdfjs-dist
2. 加载PDF文档
const pdfFile = "Your PDF file path"
const pdf = await getDocument({
url: pdfFile,
cMapUrl: './node_modules/pdfjs-dist/cmaps/',
cMapPacked: true
}).promise
3. 获取文本信息
假如需要读取第一页的文本信息, 首先通过 getPage 获得第一页文档信息的引用.
const pageNumber = 1 //假定我们需要获取第一页的文本
const page = await pdf.getPage(pageNumber)
然后通过 getTextContent().items 获得页面中的文本信息.
const textContent = await page.getTextContent()
console.log(textContent.items)
每一个item中, 代表页面里的一小块文本的信息, 例如
{
"str": "Rearranging to avoid summing the infinite tail of the distribution...",
"dir": "ltr",
"width": 269.9515540245999,
"height": 10.1,
"transform": [10.1, 0, 0, 10.1, 108.1, 375.5],
"fontName": "g_d0_f2",
"hasEOL": false
}
字段说明
| 字段名 | 用途 |
|---|---|
| str | 文本内容 |
| dir | 文本的方向: ttb(top to bottom), ltr(left to right) , rtl(right to left) |
| width | 宽度 |
| height | 高度 |
| transform | 用于描述文本项的变换矩阵. 最后两个数字(108.1,375.5)分表代表字体所在位置的X,Y坐标 |
| fontName | PDF内部的字体风格代号, 需要转换才能得到真正的字体名字 |
三. 文本信息的计算
从上面TextContent的item里, 已经能得到文本的坐标和文字信息.
当翻译完成后, 就可以基于这两个信息把译文输出到翻译后的文档. 但是其实可以做得更好
1. 获取文本的字体信息, 使用原字体来输出译文
const fontFace = page.commonObjs.get(item.fontName)
fontFace包含字体的名称、类型和二进制数据等字体信息。字体信息的作用主要有两个:
- 基于字体的名称,在网上搜索下载网站的字体。为了减少PDF文件的大小,PDF文档中的字体自包含文档中用的的字符数据。
- 判断译文的字符是否能够使用原文的字符打印。每个字体都有自己所支持的字符列表。例如,一个英文字体是不支持中文字符的。直接打印会导致PDF中显示方块。
- 计算译文的长度。当译文过长时,需要根据长度等比压缩字体的大小。
2. 文本的打印角度
不是所有的文本都是水平的,有些可能是以45度角打印的。
根据transform字段可以计算实际的打印角度。
const angle = Math.atan2(item.transform[1], item.transform[0])
3. 获取文本的颜色, 使用原字体颜色来输出译文
计算文本颜色是一个比较复杂的问题,在网上的资料也很少。
首先需要了解PDF的指令。简单来说,PDF页面其实是执行一系列指令后生成的结果。例如:
setTextMatrix [100,100, 200,300] // 指定文字区域
setFillRGBColor [255, 255, 0 ] // 设置填充颜色为黄色 RGB(255,255,0)
moveText [125, 130] // 移动坐标到(125, 130)
drawText [{chat:xx...}...] // 在当前坐标输入文字
可以看出,文字的颜色和位置是由输出文字的那一刻的填充颜色决定。因此,要计算一个item的颜色,需要以下两步:
- 定位item在指令流中的位置。
- 解析颜色相关的执行指令,计算输入item文字那一刻的填充颜色。
首先, 获取一个页面的指令流,并打印出来用于学习和观察。(带着目的去观察, 个人觉得比较好的学习方式)
const operatorList = await page.getOperatorList()
for (let fnIndex = 0; fnIndex < operatorList.fnArray.length; fnIndex++) {
const fn = operatorList.fnArray[fnIndex]
const args = operatorList.argsArray[fnIndex]
// 打印执行流的信息, 包括 fn->函数名, args->函数参数
console.log(Object.keys(OPS).find(key => OPS[key] == fn), args)
}
然后,根据文本位置变化和颜色变化的相关指令计算item所在位置以及当时的填充颜色。
这个问题的详细说明已经在代码中了,所以不再展开描述。
export function getItemColor(item: TextItem, operatorList: PDFOperatorList) {
// 记录状态的堆栈
const stack: PDFStatus[] = []
// 当前状态记录
let currentStatus: PDFStatus = {}
// 按顺序分析页面指令
for (let fnIndex = 0; fnIndex < operatorList.fnArray.length; fnIndex++) {
const fn = operatorList.fnArray[fnIndex]
const args = operatorList.argsArray[fnIndex]
switch (fn) {
//保存
case OPS.save:
stack.push(currentStatus)
currentStatus = { ...currentStatus }
break
//还原
case OPS.restore:
currentStatus = stack.pop() ?? {}
break
//设置文本填充颜色
case OPS.setFillRGBColor:
currentStatus.currentColor = [args[0], args[1], args[2]]
break
//设置文本区域
case OPS.setTextMatrix:
currentStatus.currentMatrix = [args[4], args[5]]
currentStatus.currentXY = [args[4], args[5]]
break
//设置行距
case OPS.setLeading:
currentStatus.leading = args[0]
break
//设置字体类型和大小
case OPS.setFont:
currentStatus.font = [args[0], args[1]]
break
//计算换行, 换行时当前坐标需要跳到下一行的开头
case OPS.nextLine:
case OPS.nextLineShowText:
case OPS.nextLineSetSpacingShowText:
if (currentStatus.leading && currentStatus.currentXY) {
currentStatus.currentXY = [currentStatus.currentXY[0], currentStatus.currentXY[1] - currentStatus.leading]
}
break
//移动文本坐标
case OPS.moveText:
if (currentStatus.currentXY) {
currentStatus.currentXY = [currentStatus.currentXY[0] + args[0], currentStatus.currentXY[1] + args[1]]
}
break
//显示文本
case OPS.showText:
if (currentStatus.currentXY) {
let x = currentStatus.currentXY[0]
let y = currentStatus.currentXY[1]
// 判断文本是否匹配定位
const isMatch = () =>
Math.abs(x - item.transform[4]) < item.height / 5 && Math.abs(y - item.transform[5]) < item.height / 5
if (isMatch()) {
return currentStatus.currentColor
}
if (args[0]) {
// 计算打印的每个字的实际坐标, 然后和item的坐标进行配对
for (let charInfo of args[0]) {
if (typeof charInfo?. width == 'number' && currentStatus.font) {
if (isMatch()) {
return currentStatus.currentColor
}
x += (charInfo?. width / 1000) * currentStatus.font[1]
} else if (typeof charInfo == 'number' && currentStatus.font) {
if (isMatch()) {
return currentStatus.currentColor
}
x -= (charInfo / 1000) * currentStatus.font[1]
}
}
}
}
break
}
}
}