解决依赖内嵌字体才能正常显示的电子书乱码问题

有一位名为“行云”的小伙伴留言反馈了一个问题,说他在网上找到一本 AZW3 格式的电子书,但是它使用了特殊的字体,只要转换成其它格式就会乱码,并且也不能正常复制字体,问如何解除这种限制。

一、问题表现

简单实测了一下样本电子书,发现在 Kindle 设备或 Calibre 内置阅读器中阅读一切正常:

但是在使用 Calibre 将其转换成 MOBI 格式后,发现内容中大部分文字都变成了“乱码”:

用 KindleUnpack 把样本电子书拆解成源码后,发现里面实际的文本内容确实是“乱码”:

另外,还在源码的 Fonts 文件夹中发现了一枚字体文件,在 CSS 文件中也可以看到强制指定该字体的相关属性。经测试,电子书中的内容一旦应用该字体,内容就会变得正常刻度,否则就呈乱码状。

这看似是个很奇怪的问题,但是并不是太难解决,下面我们先来分析一下到底是怎么回事。

二、问题分析

一本 Kindle 电子书想要被正常阅读,需要遵循亚马逊为其制定的实现标准,这些实现标准都是公开的透明的(你可随时可以通过亚马逊持续维护着的《Kindle 电子书发布指南》来了解相关信息),因此不可能脱离这个实现标准对电子书做什么额外操作,除非是利用实现标准中所具备的特性搞一些障眼法。

这位小伙伴所提供的样本电子书便利用了实现标准的一项本应是提升阅读体验的特性,有(恶)意地对电子书内容做了混淆,使得电子书必须依赖字体才能正常显示,这一特性就是“嵌入字体”。

正常情况下“字符”与字体中的“字形”应该是一一对应的。如果一个字符是“人”,那么应用字体后的字形看起来也应该是“人”。而该样本电子书却没这么做。假设有一句话是“人人为我,我为人人”,它就会将其故意写成“㭃㭃㘣㝒,㝒㘣㭃㭃”,然后再通过修改字体文件,将里面的字符和字形的故意错误映射,如字符“㭃”映射到“人”字形、字符“㘣”映射到“为”字形等。这样你就只能依赖这个字体文件才能看到那句话的正确显示“人人为我,我为人人”,但实际文本的字符却是“㭃㭃㘣㝒,㝒㘣㭃㭃”。

用字体编辑软件 FontForge 打开在源代码中的字体,就可以看到很多字符与字形的错乱映射:

▲ 每个方框上方的小字是“字符”,下方的大字是与字符相对应的“字形”

在上图中,按从左到右、从上到下的顺序,可以看到“㕟㕡㕣㕤㕿㖂㖐㖓㖚㗊”这十个字符,其对应字形却是“提场展因大世意事没年”。这就是为何电子书内容是乱码,应用字体后却是正常的原因。

该样本电子书正是挑选了一些常用的高频字符,将它们的字形根据制作者自己制定的规则逐一映射到“乱码”字符上,然后再根据这个规则反向把电子书中的正常字符替换成“乱码”字符。这样,通过把字体嵌入到电子书中,并在 CSS 中为“乱码”内容指定该字体,就可以让乱码内容正常显示了。当时当你得到这样一本经过处理的电子书后,却无法对它进行编辑和格式转换,更不能正常使用标注、查词等功能。

三、解决方法

知道了问题所在,解决方法就显而易见了,我们只需要将乱码字符替换成他所对应字形的正确字符即可。大体思路为:先整理出字体文件中所有映射正常字形的乱码字符,然后把这些乱码字符所映射的字形抄下来(也就是将其转化成字符),并使其与乱码字符逐一配对,制成替换规则,最后用 Calibre 转换功能中的“查找替换”功能把乱码字符替换成正常字符。下面就以样本电子书文件为例说一下具体步骤。

1、确定字符范围

首先用 FontForge(也可使用其它字体编辑软件)查看样本电子书所使用的字体文件,借助 Unicode 十六进制编码确认错误映射字形的乱码字符范围。不同字体文件其范围会有所不同,个数可能比较多也可能比较少,分布可能比较集中也可能比较分散,但是只要是字符和字形不匹配就是需要筛选出来的。

比如在本例中,字符的分布比较集中,所以可以很方便的确定它们的范围。如上图所示,第一个出现错误映射字形的字符是“㐨”,选中它就可以在软件界面上方看到 Unicode 编码 0x3428,用同样的方法找到最后一个字符的 Unicode 编码 0x4dbc,这样就可以确认这些乱码字符的范围是从 0x3428 到 0x4dbc。

2、导出字符编码

确定好范围后,接下来就要获取这个范围内所有字符的 Unicode 十六进制编码,以便在查找替换时匹配它们。不过需要注意,并不是每一个字符都含有字形的,我们所需要的只是含有字形的字符。

为了避免重复的手工操作,可以使用一个名为 fonttools 可处理字体的三方 Python 库来提高效率。如果你的电脑已装有 Python 及其包管理器,可直接使用 pip 命令在命令行中运行以下指令安装该库。

pip install fonttools

安装完成 fonttools 后,就可以在命令行上使用 ttx 命令执行下方的指令,来导出字体的 CMAP 表(即字符和字形的映射索引)以批量获取字体文件中所有字符编码,并且会自动忽略无字形的字符。

ttx -t cmap font.otf

* 注意!指令中的 font.otf 要换成你自己拆解出来的字体文件名

执行完命令后,可以在字体文件所在目录看到命令生成名为 font.ttx 的 XML 文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<ttFont sfntVersion="OTTO" ttLibVersion="3.44">

  <cmap>
    <tableVersion version="0"/>
    <cmap_format_4 platformID="3" platEncID="1" language="0">
      <map code="0x20" name=""/><!-- SPACE -->
      <map code="0x21" name=""/><!-- EXCLAMATION MARK -->
      <map code="0x22" name=""/><!-- QUOTATION MARK -->
      <map code="0x23" name=""/><!-- NUMBER SIGN -->
      <map code="0x25" name=""/><!-- PERCENT SIGN -->
      <!-- 此处省略若干条 -->
      <map code="0xff1a" name=""/><!-- FULLWIDTH COLON -->
      <map code="0xff1b" name=""/><!-- FULLWIDTH SEMICOLON -->
      <map code="0xff1f" name=""/><!-- FULLWIDTH QUESTION MARK -->
      <map code="0xff5b" name=""/><!-- FULLWIDTH LEFT CURLY BRACKET -->
      <map code="0xff5d" name=""/><!-- FULLWIDTH RIGHT CURLY BRACKET -->
    </cmap_format_12>
  </cmap>

</ttFont>

这是字体文件中所有字符的 Unicode 编码,接下来需要提取之前确定的范围并转制成替换规则。

3、转制替换规则

本例中之前确定的字符范围是 0x3428 到 0x4dbc,所以从 XML 中把这个范围内的字符编码提取出来。

<map code="0x3428" name=""/><!-- CJK UNIFIED IDEOGRAPH-3428 -->
<map code="0x343c" name=""/><!-- CJK UNIFIED IDEOGRAPH-343C -->
<map code="0x343d" name=""/><!-- CJK UNIFIED IDEOGRAPH-343D -->
<map code="0x3445" name=""/><!-- CJK UNIFIED IDEOGRAPH-3445 -->
<map code="0x344d" name=""/><!-- CJK UNIFIED IDEOGRAPH-344D -->
<!-- 此处省略若干条 -->
<map code="0x4d56" name=""/><!-- CJK UNIFIED IDEOGRAPH-4D56 -->
<map code="0x4d99" name=""/><!-- CJK UNIFIED IDEOGRAPH-4D99 -->
<map code="0x4dae" name=""/><!-- CJK UNIFIED IDEOGRAPH-4DAE -->
<map code="0x4db6" name=""/><!-- ???? -->
<map code="0x4dbc" name=""/><!-- ???? -->

想要把这些 XML 格式的字符编码转换成 Calibre 替换规则格式还需要处理一下。可以用 Sublime Text 之类的代码编辑器,开启正则替换模式,查找 ^.*"?0x(.*?)".* 替换成 [\\u$1]\n\n。替换完成后,这些字符的 Unicode 编码就已经按照 Calibre 替换规则格式每间隔两个空行排列好了,如下所示。

[\u355f]


[\u3561]


[\u3563]


[\u3564]


[\u3572]


[\u357f]


[\u3590]


[\u3593]


[\u359a]


[\u35ca]

* 提示:这里替换的目的是将字符的 Unicode 编码转换成匹配字符的正则表达式

当然现在这个替换规则文件只能匹配乱码字符,下面还要为其填充替换内容。现在先这些内容另存一下,文件名随意,后缀名为 .csr(即 Calibre 的替换规则文件格式),如 pattern.csr。

4、填充替换规则

填充替换规则是个力气活。仍以之前图示显示的“㕟㕡㕣㕤㕿㖂㖐㖓㖚㗊”这十个字符为例。

你只需要按照 FontForge 中从左到右、从上到下的顺序,将字符编码对应字形输入在其下方即可。注意,一个字符编码及其对应字形所代表的真正字符为一组,每一组之间有一个空行,如下所示。

[\u355f]
提

[\u3561]
场

[\u3563]
展

[\u3564]
因

[\u3572]
大

[\u357f]
世

[\u3590]
意

[\u3593]
事

[\u359a]
没

[\u35ca]
年

这些抄写工作比较枯燥(本例有 400+ 个字符),不过对于打字熟练的人来说,输入几百个常用汉字应该花不了几分钟的时间。当然,如果你想要节省时间,或者说电子书制作者使得每本电子书的字体都不一样,那可能就需要 OCR 相关的编程技术等来提高效率了,不过限于篇幅,这里不便展开讨论。

编辑完成后保存一下。现在你就得到了一个可以把电子书的“乱码”恢复成正常字符的替换规则文件。

* 提示:这是本例最终编辑好的一份替换规则文件,可供参考:百度网盘【提取码: gbvb】

5、使用替换规则

转换电子书时,在转换设置面板中,切换到【搜索替换】界面,点击上面的【加载】按钮,载入之前编辑好的 .csr 规则文件,然后点击【确定】按钮开始转换,最终得到的电子书内容就恢复正常了。

总的说来,这个问题的解决方法并不复杂,但是操作起来还是要花费一些功夫的。如果能通过其它渠道找到替代文件,感觉没必要这样去做。不过,通过这件事真是见识了“盗版界”的奇技淫巧,本来感觉往电子书里插广告就够恶心的了,这种故意混淆字符硬生生退化电子书功能的做法更是刷新了下限。

有帮助,[ 捐助本站 ] 或分享给小伙伴:

发表评论

标注为 * 的是必填项。您填写的邮箱地址将会被保密。如果是在本站首次留言,审核后才能显示。
若提问,请务必描述清楚该问题的前因后果,提供尽可能多的对分析该问题有帮助的线索。

小伙伴们发表了 6 条评论

    • 利用字体混淆内容和真正的加密是两码事。破解加密是很困难的事,除非有专业人士提供了现成的工具,普通人是搞不定的。

  1. 搅乱映射……这思路实在让我五体投地。佩服佩服。

    如果作者真心要搞,弄个随机工具全盘打乱,那就完蛋了。会玩映射的人写随机工具不是难事。

    我以为作者发现了自动修映射的方法……原来是人肉匹配啊。我宁可一页页OCR也不想修映射。毕竟已经电子化了,字号放大点,让机器OCR的正确率非常高的。

    ttx是AFDKO工具包里最好用的工具了。改个名非常方便,而且不会夹私货。比fontcreator好多了。fontfroge我实在用不了,随机崩溃,不带提醒的。

  2. 这本书我看完了…某号说是自己书友通过OCR纸质书做的电子版,实际上中亚确实是没出过电子版。做完了的结果就是只有通过kindle app、kindle阅读器、多看app才能阅读。

    感觉他们这么处理的逻辑也是清奇,估计是不想让其他号或app来发这个书吧…