Puppeteer 生成的 PDF Copy/Paste 文本产生奇怪的字符

Puppeteer generated PDF Copy/Paste text produce weird characters

我正在使用最新版本的 puppeteer 来生成附件中的 PDF。当我在 Adob​​e Acrobat 中打开它并尝试复制和粘贴文本时,它变成

This is a test string.

进入

Țħįș įș ǻ țěșț șțřįňģ.

这是我生成 PDF 的代码。

const puppeteer = require('puppeteer');
const argv = require('minimist')(process.argv.slice(2));
const fileName = argv.fileName || "page";
const timeout = 90;

(async () => {
  var pageUrl = "my-url-here"
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  
  function onTimeout() {
    console.log("Timed out waiting for data after " + timeout + " seconds.");
    process.exit();
  }
  
  console.log("Opening " + pageUrl);
  await page.goto(pageUrl, {waitUntil: 'networkidle2'});
  console.log("Waiting for page to load...");
  
  console.log("Waiting for data to load...");
  await page.waitForSelector('#print-report-loaded', {timeout:timeout*1000}).catch(onTimeout);
  
  var fileFullName = fileName + ".pdf";
  console.log("Saving PDF as " + fileFullName);
  await page.pdf({path: fileFullName});
  console.log("PDF saved successfully as " + fileFullName);

  await browser.close();
})();

Here is link to the generated PDF

如能知道如何修复,我们将不胜感激!

Acrobat 不会更改 文本,它只是复制为这些字体存储的 Unicode 字符。您看到的 'characters' 是 Type 3 轮廓,它们具有 形式 的 "regular" 字符,但它们相关的 Unicode 代码点确实是重音字符的代码点。

从AcrobatReader和官方PDF规范来看,一切都是"working as designed"。

让我们看看您的 PDF。

为了不必要地使事情复杂化,人们会认为这只需要一种字体,但您的工具生成了 两种 字体:F0,它将字符代码映射到以下 Unicode代码

<(01)> <( )>
<(0D)> <(.)>
<(26)> <(Ț)>
<(32)> <(ǻ)>
<(35)> <(ě)>
<(37)> <(ģ)>
<(38)> <(ħ)>
<(39)> <(į)>
<(3E)> <(ň)>
<(42)> <(ț)>

F1 映射到

<(15)> <(ř)>
<(16)> <(ș)>

字符代码以字符串的形式写出,一次一个字符(中间有一些命令;此处省略,因为不是很相关):

<26><38><39>{16}<01><39>{16}<01><32><01><42><35>{16}<42><01>{16}<42>{15}<39><3E><37><0D>

我指出 <..> 内配对的十六进制代码来自字体 F0{..} 来自 F1。现在,如果你用 Unicode 字符一个一个地替换字符索引,你确实得到了 Unicode 字符串

Țħįș įș ǻ țěșț șțřįňģ.

此处使用的 "fonts" 是 Type 3 PostScript 字体,完全嵌入 PDF 中。例如,字体 #0 定义为

8 0 obj @ 1059      % "F0"
<<
  /Type     /Font
  /Subtype  /Type3
  /CIDToGIDMap  /Identity
  /CharProcs    
  <<
    /g0     11 0 R      % -> stream
    /g1     12 0 R      % -> stream
    /g26    14 0 R      % -> stream
    /g32    15 0 R      % -> stream
    /g35    16 0 R      % -> stream
    /g37    17 0 R      % -> stream
    /g38    18 0 R      % -> stream
    /g39    19 0 R      % -> stream
    /g3E    20 0 R      % -> stream
    /g42    21 0 R      % -> stream
    /gD     13 0 R      % -> stream
  >>
  /Encoding     
  <<
    /Type   /Encoding
    /Differences [ 0 /g0 /g1 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /gD /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0
        /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g26 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0
        /g0 /g32 /g0 /g0 /g35 /g0 /g37 /g38 /g39 /g0 /g0 /g0 /g0 /g3E /g0 /g0 /g0 /g42 ]
  >>
  /FirstChar    0
  /FontBBox     [ -1 202 598 -801 ]
  /FontDescriptor 10 0 R        % -> FontDescriptor (Font)
  /FontMatrix   [ 0.082254 0 0 -0.082254 0 0 ]
  /LastChar     66
  /ToUnicode    9 0 R       % -> stream
  /Widths   [ 500 300 0 0 0 0 0 0 0 0 0 0 0 244 0 0 0 0 0 0 0 0 0 0 0 0
      0 0 0 0 0 0 0 0 0 0 0 0 641 0 0 0 0 0 0 0 0 0 0 0 579 0
      0 592 0 664 616 263 0 0 0 0 616 0 0 0 404 ]
>>
endobj

大部分都不重要,除了 编码数组 ,它告诉 字符索引 与什么字形名称相关联,以及CharProcs 数组,它将编码数组中的 names 与实际绘图指令相关联。这是从 "font name plus character index" 显示字符串到 "character index in encoding" 的路由,后者又使用 ToUnicode 数组查找 actual(已报告)每个字符的 Unicode 值。

每个角色本身的绘图指令(对每个 /gX 流的引用)是一系列常规的 movelinefill 指令 – 再次, 没有什么不寻常的,尽管其他 PDF 引擎更经常包含原始字体,而不仅仅是文字绘图说明。

但是 ToUnicode table 把复制的东西弄乱了。它没有声明 "character 16#26 maps to Unicode U+0054 'Latin Capital T'",而是指向 "U+021A Latin Capital T with Comma Below"——而且没有明显的原因!这肯定不是 random 翻译,但我找不到 any 合理的解释来解释为什么有人会故意这样编码纯文本......除非有人在那里满足地笑着想,"yes, this is what I had in mind" – 在这种情况下,这将是 故意混淆

Github 处的 Puppeteer 代码似乎无法处理 PDF 本身,因此它必须由 Chromium 处理,Chromium 在内部使用 Skia PDF 引擎(据报道但由 PDF 二进制 header 证实) , 当最高位被清零时,读取 "D3 EB E9 E1" – "Skia"。它已被报告为错误 as early as 2012,但有 2017 年的报告,这似乎表明这不是他们需要解决的紧迫问题。