一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

yellowladybug 发布于2年前
0 条问题

用HTML+CSS进行图书出版

最近我的新译作 《物理是什么》 出版了,这本书是日本著名物理学家,诺贝尔奖得主朝永振一郎先生的遗作,算是介绍物理和科学史的一部名作。当然,对于我来说,这本书还有另外一个意义,因为这本书是我自己用HTML+CSS排版输出的,据图灵的编辑说,这可能是图灵第一本不用InDesign排版的书,可能也是第一本由作译者自己排版的书。无论如何,用HTML+CSS排版是一件很有趣的事情,相比传统排版来说有一些优点,我想把这次的经验写下来,和大家分享。当然,在实际作业的过程中也遇到了一些问题,我也采用了一些方法去解决,我采用的方法可能不是最优解,也希望大家帮我指出来,共同进步。

本书部分源代码和脚本已作为示例发布在 GitHub 上,供大家参考。

图书出版的大致工作流程

在讲排版之前,先说说图书出版大致要经过怎样的工作流程。对于事务性的流程就不展开说了,主要说一些和文稿相关的部分。

要印书,首先当然要有原稿。原稿的格式可能千奇百怪,在数字文稿当中,最常用的大概是Word文档,Mac用户可能会用Pages,也有些IT圈子的作者喜欢用Markdown,学术圈的可能会用TeX等等。原稿需要提交给出版社的编辑进行校对、修改和润色,编辑有自己的工作环境,所以可能大部分编辑会要求作者提供Word格式的原稿,这里就会涉及到一次格式转换。

编辑跟作者修改过一轮之后,就会把原稿发给排版公司进行排版,对于排版中的一些格式要求,编辑会在原稿里注明。排版公司用的排版软件一般是Adobe InDesign,这里又会涉及到一次格式转换,或者说是将Word原稿中的内容导入到InDesign中的过程。最后,排版公司会输出PDF文件,和编辑再进行几轮校对和修改之后,由编辑将PDF交给印制人员到印刷厂付印。

我们发现在这个流程中至少涉及两次格式转换,一次是作者自己的原稿要转换成编辑的工作格式(例如Word),还有一次是编辑的工作格式需要导入到排版软件(例如InDesign)中进行处理。我们知道,格式转换越多,里面会产生的问题也就越多,我之前的一本译作就遇到过上标莫名其妙失效的问题,明明是5 2 ,最终PDF里变成了52,被读者投诉到爆,我也觉得很冤枉。

还有一个问题就是数学公式比较多的书,就比如《物理是什么》这本书,有几章里面包含大量的公式,这种书编辑都比较头大,因为排版公司经常会在公式上出毛病。在国际学术出版领域,一般都用高德纳大神的TeX来排版,作者一般也接受用TeX来写原稿,而众所周知地,TeX对公式的支持是非常棒的,于是整个流程中只需要用TeX就可以一站式解决问题了。不过,在非学术出版领域,TeX实在是没什么人用,据我所知,公式一般还是用Word的公式编辑器来进行编辑,最后导入到InDesign来处理的。我对InDesign不熟,也不知道它如何处理Word的公式,不过编辑经常跟我抱怨公式会出问题,看来InDesign在这方面做得也不咋地。

为什么选择HTML+CSS

从这本书来说,我之所以决定尝试一下自己排版,初衷也是为了把数学公式排得更好一些。其实,排版公司不是不能排公式,但我上面说过,在几次格式转换过程中,公式很容易出问题,如果书里公式很多,一个一个检查起来就特别费力,问题也不容易被发现。其实在出版领域有很多工具可以替代InDesign,最有名的当然就是TeX。我曾经想过直接用TeX,毕竟对数学公式支持很好,但TeX我完全没有接触过,如果从头学起来感觉代价有点大。后来我发现还有HTML+CSS这个选项,很多作译者朋友跟我说起,国外有一些技术图书就是用HTML+CSS排版的,我想既然在国外有这方面专业运用的经验,那么就意味着,HTML+CSS完全可以胜任专业出版的需求,既然如此何不试一试呢?

选择HTML+CSS的几个原因:

  • 对于有网页经验的程序员(比如我)来说学习成本很低。

  • 在国外有一定的专业出版运用案例。

  • 可以通过Javascript进行高级渲染,比如用 MathJax 渲染数学公式。

  • 可以无缝对接Markdown格式的原始文稿(虽然这次我没用)。

  • 可以用同一套源文件输出不同的格式,例如Word、EPUB。

  • 最重要的,符合程序员的习惯和美学。

HTML+CSS排版的软件栈

基本架构

毋庸置疑,HTML+CSS这个体系中,HTML是文稿,CSS是样式,我们需要一个渲染器将它们渲染输出成PDF。我阅读了国外的一些技术文档,普遍推荐使用 Prince 来进行渲染输出。于是,一个最基本的软件栈是这样的:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

需要指出的是,Prince对于商用是需要购买授权的,单机版本的授权费用是495美元,这个价格不便宜,除此之外还有服务器授权等方式,具体看官方的 购买页面 。在不购买授权的情况下,Prince也提供 免费的非商用版本 ,但非商用版本生成的PDF文件会在第一页的右上角加上一个水印。当然,如果你一定要想个取巧的办法,可以在第一页生成一个空白页,然后再把这一页从生成好的PDF中去掉。实践证明这是一个在 技术上 可行的方案,但这样的做法是违反软件 许可协议 的,因为许可协议规定不得删除非商用版本生成的水印,也不得将非商用版本用于商业用途。

Prince的命令行很简单,基本用法如下:

prince source1.html source2.html ... -o output.pdf

如果你的HTML源文件中没有引用CSS,也可以通过命令行指定一个外部的CSS文件:

prince source1.html source2.html ... -s styles.css -o output.pdf

关于Prince命令行用法,可以参考 Prince官方文档

PDF后处理

无论是否要用上面这个取巧的办法,我们依然还是有必要对Prince生成的PDF加上一个后处理的步骤,因为出版社需要在正文的PDF前面加上一页扉页,扉页是由封面设计公司来制作的,我们只要把封面设计公司交付的一页PDF插到正文PDF前面就可以了。对PDF进行后处理有很多工具,我的习惯是写一个Python脚本,对PDF的处理使用了 PyPDF2 包。

HTML预渲染

这本书需要使用MathJax渲染数学公式,但Prince本身对Javascript的支持非常有限,因此需要对包含数学公式的HTML文件用 PhantomJS 进行预渲染。除了数学公式之外,预渲染脚本还可以用来处理其他一些Javascript渲染操作,比如自动为中文和英文之间加上空格等等,只要能够用Javascript实现的都可以,这为我们的排版输出工作增加了更多的灵活性,也减少了手工劳动。

关于HTML预渲染的细节,我放在后面介绍。

编辑的工作文档

现在我们可以将HTML+CSS直接渲染成PDF,但我在前面的工作流程中提到,编辑需要一个工作文档,以便进行校对和润色修改。在我们这个场景中,理想的情况是编辑可以直接在HTML文档上工作,然后通过git之类的版本管理工具来协作,但现实情况是,编辑似乎只喜欢用Word,HTML对于编辑来说,里面的干扰信息太多。Markdown应该也比较适合编辑工作,毕竟跟纯文本差不多,如果编辑能接受的话,用Markdown来撰写和编辑原稿是也是个不错的选择。

我这次最终是选择了用 Pandoc 将HTML转成Word文档(docx),然后编辑在Word里修改之后,我再根据Word里的修订记录去改HTML原稿。显然,这种方式非常兜圈子,也是整个流程中看起来最难受的部分,不过这次暂且先这样妥协一下吧,实际上如果修改不多的话,并不是很影响效率。值得一提的是,Pandoc会自动将HTML中嵌入的LaTeX公式转换成Word的公式(需要在命令行中开启 -f tex_math_dollars ),所以编辑可以直观地看到公式的效果(但需要跟编辑说明,Word里的公式不是最终渲染效果)。此外,Pandoc还可以输出EPUB,但我没用过,希望熟悉的朋友补充分享。

Pandoc还支持用Python脚本编写自定义过滤器,这部分内容比较复杂,具体参见 官方文档

完整架构

加上上面这些要素之后,完整的软件栈和流程差不多是这样的:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

在讲具体的技术细节之前,我想先说明一点,这篇文章的重点是技术分享和探讨,以及记录和整理一些思路,我假定阅读本文的读者对HTML、CSS已经有足够的了解,因此这篇文章只谈HTML+CSS在出版领域的运用实例,不会过多地介绍一些基本概念,希望大家理解。

基本排版

首先,我们来大致了解一下HTML+CSS排版的基本思路。一般的HTML是作为网页在浏览器中展示的,网页是连续滚动的,不存在分页的概念,而印刷排版与网页的一个最大的区别就是印刷排版需要分页。所谓分页(paging),其实可以理解为将网页的内容装进一个一个称为页面(page)的容器里,每个页面有固定的宽度和高度,连续的内容装进页面时,还需要考虑一些特殊的分页策略。此外,页面中还存在如页眉、页脚、页码、脚注等特殊区域,这些也是与浏览器中展示网页不同的地方。除了这些地方之外,网页布局设计的基本思路依然是通用的。

之前我们也提过,HTML+CSS体系中,HTML负责控制内容,而CSS负责控制样式,因此排版的重点是如何写好CSS。在CSS3中加入了一个Paged Media模块,负责控制分页媒体(而不是屏幕浏览器这种连续媒体)的样式,而印刷就是一种分页媒体。关于Paged Media模块的详细资料,可以参考 W3C的官方文档

除了标准的CSS语法和属性之外,Prince还提供了一些自定义的属性(以 prince- 开头)。关于这些自定义属性,可以从 Prince的文档 中找到,不过Prince的文档一直被诟病做得很差。

页面定义

对于分页媒体来说,我们首先需要定义页面的尺寸,这个有点类似于Word里面的“页面设置”,在CSS3中我们使用 @page 规则来定义页面,例如:

@page {
   size: 145mm 210mm; /* 32开 */
   margin-top: 20mm; /* 天头 */
   margin-bottom: 15mm; /* 地脚 */
   margin-inside: 21mm; /* 订口 */
   margin-outside: 16mm; /* 切口 */
   marks: crop cross; /* 裁切(出血)标记,中心十字标记 */
   prince-bleed: 3mm; /* 出血区 */
   prince-trim: 10mm; /* 出血区之外的白边 */
}

在上面这段样式中, size 代表页面的尺寸,这取决于采用多大的开本,一般是由编辑来定。 margin- 开头的属性大家应该很熟悉,但 -inside 和 -outside 属性是页面媒体中所特有的。我们知道,一本书翻开之后,页面是分左右的,对于左侧的页面来说,它的装订线在右边,而对于右侧的页面来说,它的装订线在左边,而内侧(inside)指的就是有装订线的一侧,外侧(outside)指的就是没有装订线的一侧。对于一本书来说,内侧和外侧的页边距是不同的,内侧的边距( margin-inside )术语叫做 订口 ,外侧的边距( margin-outside )术语叫做 切口 ,订口因为需要留出装订空间,因此需要比切口设置得大一些。

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

图片来源:

https://zhuanlan.zhihu.com/p/20645708

prince-bleed 和 prince-trim 是两个Prince的自定义属性,但其实CSS3也有对应的标准属性。其中bleed一般直译为 出血 ,这块区域的位置看下图:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

图片来源:

http://www.cm020.com/index.php?/detail/88/702

为什么要做出血呢?因为图书印刷的时候,实际印刷的纸张比我们在 size 属性中设置的大小要大,印好之后再裁切成 size 属性中设置的大小。之所以要这样做,是因为印刷不可能做到无边距,当需要印刷填满页面的图案(比如整版图片或者底纹)时,就需要故意多印出来一块,然后再裁掉,这样最终的效果就可以让图案填满整张纸,而不会留下白边。而出血指的就是这块故意多印出来会被裁掉的区域,按照排版惯例,都要设置3mm的出血区。 注意:即便你的内文没有这种填满页面的情况(内文本身就有边距),也需要按照规定设置出血区。

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

图片来源:

http://www.cm020.com/index.php?/detail/88/702

trim 指的是在出血区之外,还要在PDF中留出多大的白边,这个白边区域中可以根据需要放置一些标记,比如套色定位标记,一般黑白内文排版对这个白边没有特殊要求。 marks 指的是在页面上显示哪些标记,其中 crop 指的是上图中的出血线,也叫裁切线,印好之后的纸张就是按照这些线来裁切的。 cross 指的是十字标记,用来标记页面的中间位置。

@page 规则也可以使用伪类和伪元素,比如对于奇偶页可以用 :left 和 :right 来区分(比如说奇偶页需要在不同的位置显示页码),还有一个 :blank 可用来定义空白页的样式,此外还可以用 :nth(n) 定义具体某个页面的样式。 除了伪类之外,还可以使用标识符对某些页面进行命名,并将其用作CSS选择器,名称选择器和伪类可以并用,这一点和一般的CSS选择器是类似的。例如下面的规则将应用于名称为 main 且位于左侧的页面:

@page main:left {
  /* main左侧页面的样式 */
}

对于任何正文元素,都可以通过 page 属性指定它的页面样式。比如说,我希望目录部分的页面样式和正文不一样,那么我可以将目录区域放在一个 div 容器里,然后将这个容器的 page 属性设置为某个名称,比如 toc ,然后通过 @page toc 选择器就可以指定目录页面的样式了:

<div class="toc">
  <!-- 目录的内容放在这里 -->
</div>
div.toc {
  page: toc;
}

@page toc {
  /* toc页面的样式 */
}

页眉和页脚

CSS Paged Media模块为页面定义了一系列页边框(page-margin box),顾名思义就是在页边距部分中分配了一些特殊区域出来,这些区域可以用来放页眉、页脚以及你所需要的任何内容。页面上一共有16个页边框,每个框都有自己的名字,如图:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

图片来源: https://www.quackit.com/css/at-rules/what_is_a_page-margin_box_in_css.cfm

我们可以用 @ 规则在 @page 页面样式中设置这些框里的内容,比如我们可以在顶部显示书名:

@page {
  @top-center {
    content: "物理是什么"; /* 页边框的内容 */

    /* 也可以设置其他样式 */
    font-size: 0.8rem;
    font-weight: bold;
  }
}

当然,页眉很少显示一个固定的值,一般是需要显示当前章节的名称,便于读者翻阅查找。当前章节名称是一个动态内容,我们可以使用Paged Media模块中提供的 string-set 功能来实现。 string-set 类似于设置一个命名变量的值,然后我们可以在需要的位置来显示这个值。拿章节名称来举例,我们可以规定用 <h1> 标签显示章节名称,然后在 h1 的样式中更新变量的值。看下面这个例子:

<h1>第一章</h1>
h1 {
  /* 设置变量chaptitle值为当前h1标签的内容(章节名称) */
  string-set: chaptitle content();
}

@page {
  @top-center {
    content: string(chaptitle); /* chaptitle的值 */
  }
}

如果要显示页码,我们可以用CSS提供的一个 page 计数器,这个计数器是自动累加的,我们只需要显示它的值即可。下面是一个简单的例子:

@page {
  @top-right-corner {
    content: counter(page); /* 显示page计数器的值 */
  }
}

除了内置的页码计数器,我们还可以设置自定义的计数器来计算章号、节号、图号等等。关于CSS中计数器的使用,可进一步参考 MDN的文档

分页规则

图书排版时会遇到很多特殊的分页规则,拿《物理是什么》这本书来说,主要有这些基本需求:

  • 章标题( <h1> ):需要单独占一页,而且必须放在右侧页,章标题后面也要空一页,确保正文从右侧页开始。

  • 节标题( <h2> ):需要另起一页。

  • 小节标题( <h3> ):不能出现在一页的最后一行。

对于这种特殊的分页规则,我们可以在相应的HTML元素的样式中使用 page-break-before 和 page-break-after 属性来定义。

顾名思义, page-break-before 是定义要不要在前面分页,而 page-break-after 则是定义要不要在后面分页,这两个属性可以取的值有: always 、 avoid 、 left 和 right 。其中, always 和 avoid 比较容易理解,前者表示强制分页,后者表示避免分页,之所以用“避免”这个词,是因为有时候会出现无法避免的情况,这时候还是被会自动分页。

left 和 right 其实是比较有歧义的,一开始我也理解错了,我以为 left 是代表“如果当前页为左侧页时强制分页”( right 以此类推),但后来我又看了一下 Prince的文档 ,发现其实这个逻辑是这样的:

  • 对于 page-break-before 来说, left 的意思是:在当前页 之前 分页,如果分页之后 当前页 不为左侧页,则在 前面 插入一个空白页,确保 当前页 为左侧页。 right 以此类推。

  • 对于 page-break-after 来说, left 的意思是:在当前页 之后 分页,如果分页之后 下一页 不为左侧页,则在 后面 插入一个空白页,确保 下一页 为左侧页。 right 以此类推。

也就是说, page-break-before 确保的对象是 当前页 , page-break-after 确保的对象是 下一页 ,这个逻辑特别绕,我们看一个例子。对于章标题的分页规则,可以这样定义:

h1 {
  page-break-before: right; /* 如果当前页不是右侧页,就在前面插入一个空白页 */
  page-break-after: right; /* 如果下一页不是右侧页,就在后面插入一个空白页 */
}

对于节标题来说,我们只需要让它另起一页,那么只要强制在节标题之前分页就可以了:

h2 {
  page-break-before: always; /* 节标题另起一页 */
}

对于小节标题来说,我们不想让它出现在最后一行,那么就需要避免在它的后面分页:

h3 {
  page-break-after: avoid; /* 小节标题后面避免分页 */
}

定义上面的规则之后,如果小节标题正好出现在一页的最后一行,那么渲染器就会把小节标题挪到下一页的开头,以避免在它后面分页。

除了定义前后的分页规则,还有一个 page-break-inside 属性,它只有一个值 avoid ,这个很容易理解,比如一张表格,我们希望避免在中间分页,就可以用这个属性来定义。

分栏

分栏也是一种常用的排版需求,图书里面比较少见,但报纸、杂志、海报、册子等形式中比较常见。Paged Media模块对分栏提供了支持,一个简单的例子:

body {
  column-count: 3; /* 分3栏 */
  column-gap: 2em; /* 栏间距两个字 */
}

分栏时还会遇到跨栏元素,比如标题是横跨所有栏的,对于这种情况,一种方法是将分栏设置在段落( <p> 标签)样式上,另一种方法是在标题元素中设置 column-span: all 。

脚注

脚注是一块特殊的命名区域 footnote ,和页边框一样,也是Paged Media模块预置的区域,我们可以用CSS的 float 属性把特定的内容填充(转移)到脚注区。一个简单的例子:

<p>
  利用这些数据,开普勒获得了他的第一个成果,即对火星轨道平面
  <span class="fn">每颗行星的轨道都各自形成一个平面,称为轨道平面。</span>
  与地球轨道平面的夹角进行了精确的计算,同时也计算出了
  两个轨道平面交叉时所形成的交线的方向。
</p>
.fn {
  float: footnote;
  counter-increment: footnote; /* 脚注号计数器+1 */
}

在上面的例子中, <span class="fn"></span> 中的内容被填充到了脚注区域。按照排版惯例,脚注一般需要用楷体,字号和行距略小于正文,这些都是CSS标准样式,在此不再赘述。

脚注号是通过预置计数器 footnote 进行累加的,累加的方式可以自行定义。本书是采用按章编号的方式,也就是说,每章的开头将脚注号重置到1,在一章中脚注是连续编号的,要实现这种设计可以在章标题 <h1> 的样式中重置脚注号计数器:

h1 {
  counter-reset: footnote;
}

还有另一种常见的方式是每页都重置脚注号,只要在 @page 规则中重置脚注号计数器即可:

@page {
  counter-reset: footnote;
}

除了脚注本身的样式,我们还可以通过 @page 页面样式中的 @footnotes 规则定义整个脚注区域的样式,其中比较有用的可能是脚注区域与正文区域之间的分隔线,按照排版惯例,这条分隔线不是从左到右完整的一条线,而是只有一部分,类似下面这样:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

对于这种样式,我们可以用 border-clip 属性来实现,例如:

@page {
  @footnotes {
    border-top: thin solid black; /* 脚注区域顶分隔线 */
    border-clip: 6em; /* 顶分隔线裁切到6个正文字符长度 */
  }
}

我们还需要定义一下正文中的脚注引用标号的样式,脚注引用标号就是上图中第三行“均衡点”后面那个上标的1。默认样式中引用标号不是上标,而且与正文文字之间没有间距,我们需要调整一下样式以便符合排版惯例。引用标号的样式可以通过脚注类的 ::footnote-call 伪元素来进行定义,例如:

.fn::footnote-call {
  content: counter(footnote); /* 此处可自定义脚注号的内容,比如两边加上括号 */
  font-size: 0.8rem;
  vertical-align: super; /* 上标 */
  line-height: none; /* 避免上标对正文行距的影响 */
  margin-left: 0.25em; /* 与左右正文的边距 */
  margin-right: 0.25em;
}

除了正文中的引用标号,脚注区当中的脚注号的样式也是可以自定义的,方法是设置脚注类的 ::footnote-marker 伪元素,具体代码不举例了。

关于脚注还有一个小问题需要注意,如果在某一页的正文的最后一行出现脚注引用,这一页下面就没有足够的空间显示脚注,默认情况下,脚注会被挪到下一页,但一般的排版惯例要求脚注和引用脚注的正文要放在同一页,这时需要把那一行正文也一起挪到下一页。这种情况在CSS的标准属性中没有控制,Prince提供了一个自定义属性 prince-footnote-policy ,值得注意的是,这个属性需要设置在正文段落的样式中,而不是脚注的样式中,比如:

p {
  prince-footnote-policy: keep-with-line;
}

关于这个自定义属性,可参考 Prince的文档

更多文档

上面讲到的页眉、页脚、页码、脚注都属于生成内容(generated content),除了这些内置的区域之外,我们还可以根据需要自定义新的区域,并控制文档流中的部分内容在这些区域中进行填充。关于Paged Media模块中有关生成内容的更多功能和详细文档,可参考 W3C的官方文档

字体

字体是排版中非常重要的一个元素,在这次排版中遇到的很多问题也都是和字体相关的,所以有必要单独开一个大标题来整理一下。

字体清单

首先应该注意的是,出版行业对字体的版权要求很严格,编辑告诉我,人民邮电出版社有一套已购买了授权的字体清单,在排版中只能使用这个清单中的字体(免费字体除外)。因此,在排版的时候,应该向编辑询问一下字体授权的规范。 如果在出版物中使用了侵权的字体,是会被追究法律责任的。

这本书中使用的中文字体主要有以下这些:

  • 方正书宋:正文

  • 方正黑体:标题

  • 方正细黑:页眉、页码、目录

  • 方正楷体:注释、脚注

  • 方正仿宋:引用段落

需要指出的是,这些字体 不需要 安装到操作系统中,只要将字体文件(TTF或者其他Prince可以支持的格式)存放在一个目录中,然后在CSS中通过 @font-face 来调用即可。这样的做法可以确保项目环境的自包含性,便于维护和移交。示例:

/* 西文字体,使用系统字体Times或Times New Roman */
@font-face {
  font-family: "Latin";
  src: local(Times), local("Times New Roman"); /* 调用系统中安装的字体 */
}

/* 方正书宋 */
@font-face {
  font-family: "SongTi";
  src: url("../fonts/FZSSJW.TTF"); /* 调用本地字体文件 */
}

字号

一般来说,图书的正文大多使用10磅字,我们可以把10磅设置为根节点样式,这样在其他样式中可以用 rem 单位来调整字号:

html {
  font-size: 10pt; /* 基准字号 */
}

.fn {
  font-size: 0.8rem; /* 脚注字号是基准字号的0.8倍 */
}

字体回退

和网页一样,印刷排版也需要妥善利用字体回退机制。所谓字体回退(font fallback),就是在同时提供多个字体的情况下,当前面的字体中找不到指定字型时,就会回退到下一个字体。一般来说,中文字体中的英文字型不是很统一,比如说宋体、仿宋体和楷体中的英文字型可能是不同的,但其实我们希望在这些中文字体中使用相同的英文字型,这时我们可以定义一个字体回退列表,将西文字体放在前面,将中文字体放在后面,利用字体回退机制,英文就可以用统一的西文字体来显示,当遇到西文字体中不存在的字型,比如中文字型时,就会回退到后面的中文字体。举个例子:

body {
  font-family: Latin, SongTi; /* 字体回退列表 */
}

上面的样式中出现了Latin和SongTi两个字体,这两个字体是在我们前面的例子中通过 @font-face 定义的。然后我们在 body 的样式中定义了一个回退列表。这样的结果是,英文字型用Times字体显示,中文字型用方正书宋字体显示。

数字

在这本书中,实际的字体回退还要更复杂一些,主要是为了解决两个问题。第一个问题是,这本书中包含很多公式,包括行内公式,而MathJax渲染公式使用的是MathJax TeX字体,这会造成公式里的数字和Times字体里的数字字型不统一。因此,我把MathJax TeX字体也加到了回退列表里,用来显示所有的数字,也就是:MathJax TeX(数字)→Times(英文)→宋体/仿宋体/楷体(中文)。但MathJax TeX字体中并不是只包含数字,为了避免干扰英文的字型,我们需要对MathJax TeX字体取一个子集:

/* 数学公式字体,用于正文中的数字 */
@font-face {
  font-family: "TeX";
  src: url("../fonts/MathJax_Main-Regular.otf");
  unicode-range: U+2E, U+30-39; /* 小数点+数字0~9 */
}

body {
  font-family: TeX, Latin, SongTi; /* 字体回退列表,数字→英文→中文 */
}

看一下数字、英文和中文混排的效果:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

特殊符号的问题

第二个要解决的问题是关于一些标点符号,最常见的是外国人名之间的中点(·)、省略号以及破折号。为什么这些符号会遇到问题呢?因为我使用的西文字体(Times)中包括这些符号的字型,而且西文字体在回退列表里是优先的,但西文字体中的符号不符合中文的规范。比如说,西文字体的中点是半角的,省略号是靠基线的,破折号是分两段的;而中文规范中中点是全角的,省略号的位置要垂直居中,破折号是连续的。看下面的一组对比:

西文中点(上)和中文中点(下):

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

西文省略号(上)和中文省略号(下):

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

西文破折号(上)和中文破折号(下):

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

大出版社对排版规范非常重视,不符合规范是必须要改掉的。要解决这个问题,我采用的方法是对西文字体取子集。通过 在线工具 可以查出这些特殊字符的Unicode编码,比如中点是 U+00B7 ,省略号是 U+2026 ,破折号是 U+2014 。看一下Unicode的码表分区,我们发现 U+0000 ~ U+1FFF 应该包含了我们要用到的所有西文字符(英文以及一些拉丁扩展字母),但中点也在这个范围内,所以我们把它单独排除:

/* 西文字体,使用系统字体Times或Times New Roman */
@font-face {
  font-family: "Latin";
  src: local(Times), local("Times New Roman");
  unicode-range: U+0000-00B6, U+00B8-1FFF; /* 排除中点(U+00B7) */
}

把这些字符从西文字体中排除之后,遇到这些字符就会自动回退到后面的中文字体,也就解决了这个问题。

不过,还有一些特殊的情况,用回退的方法不好解决,比如说“-”字符在英文里是减号和连字符(hyphen),在中文里也需要使用,叫“半字线”,使用场景包括连接人名,比如“迈克尔逊-莫雷实验”、“杨-米尔斯理论”等。中文的半字线比英文的减号要长一点,下面这一段正好展示了中文和英文同时出现这个字符的情况:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

如果用英文字型来显示中文半字线就不符合规范了,但如果把这个符号从英文字型中去掉,那么英文中用这个符号的时候也会出问题。这时我们只能对这段文字单独定义一个样式,这套样式中只使用中文字体或者只使用英文字体,比如我们可以定义一个只用宋体的样式:

<p>实际上,就在阿伏伽德罗提出这一假说之前,
  <span class="song-only">盖-吕萨克</span>
  (Joseph Louis Gay-Lussac, 1778—1850)提出了“气体反应定律”。
</p>
.song-only {
  font-family: SongTi;
}

段落排版

基本段落样式

中文排版有一些基本的段落样式,包括行距、缩进、对齐等,这本书所使用的基本段落样式是这样的:

p {
  text-indent: 2em; /* 首行缩进两个字 */
  margin-top: 0em; /* 段落前后没有额外间距 */
  margin-bottom: 0em;

  text-align: justify; /* 中文排版规范为左右对齐 */
  prince-text-justify: prince-cjk; /* 使用中日韩字符左右对齐逻辑 */
}

中文段落首行应缩进两个字,段落前后没有额外间距,这一点和英文是不同的,英文段落首行没有缩进,但一般段落之间有额外间距。对齐方式为左右对齐(justify),注意不是左对齐,是左右都要对齐。在左右对齐上,默认的对齐规则似乎有点bug,为此,Prince提供了一个自定义的 prince-text-justify 属性,将这个属性设置为 prince-cjk 可以自动调用Prince的中日韩字符对齐逻辑,对于中文排版可以更好地实现左右对齐。

孤行控制

Paged Media模块中提供了孤行控制功能。所谓孤行,就是指当一个段落被拆分到两个页面时,前一页和后一页至少应保留几行文字。说起来有点绕,其实主要意思就是一段文字被拆到两页时,落在前一页或者后一页上的行数不能太少,不然看起来可能不好看。我们可以通过 orphans 和 widows 属性来设置允许的 最少 孤行行数,其中 orphans 代表上一页孤行行数, widows 代表下一行孤行行数。至于为什么用这两个词,这个梗好像挺有意思的:orphan是孤儿的意思,孤儿是 半生没人陪;widow是寡妇的意思,寡妇是 半生没人陪。

不过据我了解,孤行控制似乎是英文排版的习惯,中文排版规范对于孤行数似乎没有要求,但是 orphans 和 widows 的默认值都是2,如果使用默认值的话,可能会产生意料之外的结果,因此我建议将这两个属性设置为1,1就代表至少允许1行孤行,也就是不干预孤行的意思了:

body {
  orphans: 1;
  widows: 1;
}

中英文混排的空格

按照中文排版规范,中英文混排时,中文字符和英文字符之间应该空一格。这个问题其实可以手动插入空格来解决,也可以通过Javascript来解决。这本书是使用了ethantw开发的 Han汉字排版框架 ,这个框架的功能很多,但有些功能可能与Prince的功能之间会存在冲突,因此这本书只用了其中的中英文混排自动空格功能。和MathJax渲染数学公式一样,Javascript代码需要通过PhantomJS来进行预渲染,关于预渲染的实现方式,我会留到后面和MathJax一起讲。

除了中英文自动空格之外,这个框架还提供了一个“标点挤压”功能。这个功能其实在InDesign甚至Word中都有所体现,简单说就是对于中文的全角标点,特别是连续标点,通过挤压其空间让它们不要沾满全角字符的位置,在视觉上消除标点过于零散的问题。这个功能理论上可以提高中文排版的美观程度,特别是InDesign其实是有这个功能的,但是我在本书中没有使用这个功能,因为我认为这个功能还有必要进一步为印刷排版进行优化,如果有条件的话,我可能会fork这个框架的代码看看怎样优化一下。

换行微调

在这本书最后的校对中,编辑提出了一些换行微调的需求,比如说破折号不能出现在一行的开头,还有一些诸如最后一行只有一个字的段落。Prince默认的换行策略已经考虑了中文,比如逗号、句号等这些标点是不会出现在一行开头的,但破折号似乎是个例外,大概默认规则没有覆盖到,需要手动进行调整。

换行微调我一开始也不知道该怎么搞,各种文档都写得不清楚,后来我去Prince的用户论坛提问,得到了开发团队的回复(Prince的论坛回复效率据说是相当高的,得到了不少好评)。开发团队的建议是设置 white-space: nowrap 样式,他们解释是,英文只能在空格的地方换行,但Prince认为每个汉字之间都有一个宽度为0的空格,因此汉字之间是可以换行的,当设置上述样式之后,这段内容之间的空格(包括宽度为0的虚拟空格)。看具体代码:

<p>
  以观察事实为依据,探求我们身处的自然界中所发生的各种现
  <span class="nowrap">象——但主要限于非生物的现象——背后的规律。</span>
</p>
.nowrap {
  white-space: nowrap;
}

上面的段落中, <span class="nowrap"></span> 中的内容尽量避免换行的(当然,如果里面内容太长,实在放不下,Prince也是会换行的),这样我们就避免了破折号出现在一行的开头,具体的效果就是之前破折号的那张图:

列表

无序列表和有序列表都是HTML里常用的元素,在排版中我们可能会遇到自定义样式的有序列表,就是前面的数字不是一般的1.2.3.,而是需要一些自定义的变化。为此,Prince提供了各种计数器样式,具体可以参见 官方文档 ,我们可以在 list-style-type 属性中引用这些样式,比如:

li.roman {
  list-style-type: upper-roman; /* 大写罗马数字:I. II. III. */
}

还有一些情况是仅通过计数器样式无法处理的,比如我要数字后面不是点,而是两边加括号,比如(1)(2)(3)这样,这种情况在这本书里出现了很多,比如这一段:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

这种情况我们可以在 ::marker 伪元素中设置相应的样式,当前列表项目的编号可以通过内置的 list-item 计数器来引用,比如:

li.bracket::marker {
  content: "(" counter(list-item) ")"; /* 带括号的列表 */
}

这里需要注意的是, ::marker 伪元素并未被主流浏览器支持,比如用Chrome打开带上述样式的HTML文件,你会看到列表还是显示1.2.3.。既然是主流浏览器不支持的伪元素,相信各位在前端开发中应该也不会接触过,因此我才在这里特别提了一下。使用 ::marker 伪元素不仅能设置列表标签的内容,也可以设置相关的其他CSS样式。

如果还需要实现更复杂的列表标签逻辑,Prince还支持使用Javascript函数,当然这本书中没有运用这一特性,具体例子请参见 官方文档

颜色和图片

这本书里面一共有10张配图,都是像素图片(PNG格式),有条件的话,使用SVG等矢量图片效果当然会更好。由于我们的内文是黑白印刷,因此图片需要先转换成灰度格式,如果内文也是彩色印刷,那么图片需要使用CMYK色空间,以避免色彩转换时出现偏色。

除了图片之外,其他和颜色有关的样式都需要使用CMYK色空间,比如说这本书里内文块状注释有一个浅灰色的底色(如上面那张图),这个颜色是这样定义的:

div.notes {
  background-color: cmyk(0,0,0,0.05);
}

CSS的 cmyk() 色彩中,每个颜色分量取值都是0到1,所以上面的样式相当于5%的灰色。

对于图片来说,主要是工作在于布局。对于面积比较小的图片,我们一般采取正文环绕的布局,这种布局在网页中也比较常见,大家应该都知道应该使用浮动( float ),不过在印刷排版中, float 的用法比网页上的变化要多一些。

首先,如果图片占不满一行需要正文环绕,那么图片应该靠切口(外侧)布局,因为订口(内侧)受到装订的影响导致图片看起来不是很方便。相比网页样式来说,Paged Media模块提供了额外的两个浮动选项 inside 和 outside ,于是对于图片我们可以这样处理:

<figure style="width: 40%; float: outside; margin-inside: 1em;">
  <img src="images/figure3.png" style="width: 100%">
  <figcaption>图3</figcaption>
</figure>

效果是这样的:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

图中这一页是右侧页,所以图片被浮动到右侧(外侧),如果是左侧页,图片就会被浮动到左侧。由于这本书的图片数量很少,因此图号没有使用计数器,而是直接写了数字上去,如果图片多并需要交叉引用的话,建议还是使用计数器来编号。

其次,如果图片需要占满一行,那么就不需要正文环绕了,但是有时候我们需要让图片浮动到页面顶部或者底部,于是Paged Media模块又提供了另外两个浮动选项 top 和 bottom ,比如:

<figure style="width: 100%; float: top; margin-bottom: 1em">
  <img src="images/figure8.png" style="width: 90%; margin-left: 5%; margin-right: 5%">
  <figcaption>图8</figcaption>
</figure>

效果是这样的:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

对于顶部和底部浮动,还有两个特殊的修饰关键字 next 和 unless-fit ,其中 next 的意思是浮动到下一页(的顶部或底部), unless-fit 的意思是如果当前页能排得下这张图就不浮动,否则就浮动到顶部或底部。此外,Prince还提供了一个自定义的 prince-snap 浮动样式,意思是如果离顶部近就浮动到顶部,离底部近就浮动到底部。

目录和交叉引用

交叉引用

有时候书中会出现类似“我们曾经在第X页中提到”这样的表述(见上一张图),这里的页码显然是无法事先得知的,因为只有排好版之后我们才知道要引用的目标内容位于第几页,而且一旦内容出现调整,这个页码也会随之改变,维护难度很大。据我所知,编辑一般也不喜欢处理这种问题,但作为引进翻译来说,如果原书中有这样的表述,我们也需要尽量还原。还好HTML本身就有交叉引用功能,在网页中我们有锚点(anchor)机制,这个机制一样可以用到印刷排版中来。

和网页中的交叉引用一样,我们需要两个部分,一个是引用的目标(锚点),一个是指向这个目标的超链接。一个简单的例子:

<p id="target1">我是引用目标</p>

<p>这是一个超链接:<a href="#target1"></a></p>

和网页中的交叉引用不同的是,这个超链接本身并没有意义(读者不能在书上点击它),我们需要为超链接赋予内容,让它显示目标所在的页码。要实现这一点,我们可以使用目标计数器(target counter):

a {
  color: inherit; /* 使用和正文一样的颜色和样式 */
  text-decoration: inherit;
  content: "第 " target-counter(attr(href), page) " 页";
}

target-counter() 有两个参数,第一个参数指定我们要获取计数器的目标( attr(href) 的意思是读取当前元素的 href 属性,也就是超链接的目标),第二个参数指定要获取的计数器的名称( page )。除了显示页码本身,我们还在前后加上了“第”和“页”,这样一来,这个超链接就可以根据链接目标所在的页码,自动显示“第X页”了,而且无论目标所在的页码如何改变,我们总是能够得到正确的页码。编辑认为这个功能非常方便,我不知道InDesign是否有这个功能,但至少可能不是很易用,不然编辑们以前也不至于一直规避这种表述方式了。

目录

目录中的页码也是通过目标计数器来实现的,方法和交叉引用没有什么区别,只要对目录所涉及的标题都分别设置好锚点id,然后在目录中加上超链接即可。目录列表本身可以使用无序列表 <ul> 来构建,本书中的例子:

<ul class="toc">
    <li><a href="#ch0">序 章</a></li>
    <li><a href="#ch1">第一章</a></li>
    <li>
        <ul>
            <li><a href="#ch1-1">1. 开普勒的摸索与发现</a></li>
            <li>
                <ul>
                    <li><a href="#ch1-1-1">火星之谜与开普勒</a></li>
                    <li><a href="#ch1-1-2">开普勒定律的提出</a></li>
                </ul>
            </li>
            <li><a href="#ch1-2">2. 伽利略的实验与论证</a></li>
            <li>
                <ul>
                    <li><a href="#ch1-2-1">惯性定律</a></li>
                    <li><a href="#ch1-2-2">自然规律与数学</a></li>
                </ul>
            </li>
        </ul>
    </li>
</ul>

不过目录中显示页码的方式有些不同,按照一般惯例,我们需要把页码统一显示在页面的右端,然后在中间填充圆点。这个需求我们可以通过CSS的 leader() 表达式来实现:

ul.toc a::after {
  content: leader(".") target-counter(attr(href), page); /* 主样式,圆点填充居右 */
}

ul.toc ul ul a::after {
  content: "   (" target-counter(attr(href), page) ")"; /* 三级目录样式,页码放在括号里 */
}

最终效果是这样的:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

数学公式

前面我提到过,数学公式排版是我这次考虑进行独立排版的重要原因。在数学公式方面,LaTeX恐怕是运用最广泛的数学公式描述语言,我们也有一些Javascript软件包可以在网页中将LaTeX渲染成美观的公式,其中最有名的一个软件包就是MathJax。这本书的数学公式也是使用MathJax渲染的,但是在印刷排版中使用MathJax可谓是一波三折,因此我把它作为单独的话题梳理一下。

MathJax概要

MathJax的使用很简单,通过CDN加载它的脚本即可,不过官方的CDN最近刚刚宣布停止运营,我使用的是bootcss提供的镜像CDN:

<script type="text/javascript" src="http://cdn.bootcss.com/mathjax/2.7.0/MathJax.js?config=TeX-AMS_SVG"></script>

其中 config=TeX-AMS_SVG 是调用预置的一个配置文件,关于配置文件我们后面再讲。加载这个脚本之后,网页中的LaTeX代码就会被自动渲染成数学公式。数学公式一般有两种样式,一种是标准公式,就像教科书上的公式一样,标准公式独立于正文之外,自己占一行,居中显示,前后留有边距。另一种是行内公式,是嵌入正文当中的公式。本书中的例子:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

上图中(5)就是标准公式,其他的都是行内公式。下面是上图内容的源代码:

<p>玻尔兹曼所发现的量通常记为$H(t)$,其定义如下:</p>

$$H(t)=\sum_{V_x,V_y,V_z}\sum_{X,Y,Z}n(X,Y,Z;V_x,V_y,V_z;t)\\
  \log n(X,Y,Z;V_x,V_y,V_z;t)\tag{5}$$

<p class="ni">右边的分布函数$n$不仅包含我们前面提到的$X,Y,Z;V_x,V_y,V_z$这6个变量,
还包含一个时间变量$t$,显然这表示分布函数是沿时间变化的。右边的两个$\sum$符号表示将变量
$X,Y,Z;V_x,V_y,V_z$的所有值,即$(2)$和$(2^\prime)$的所有值加起来。加起来之后,
左边的$H$成了一个只有时间变量$t$的函数。此外,此时对于分布函数$n$还有一个条件,
即分子总数及其总动能是需要给定相应的值的。$(5)$的右边包含位置速度分布函数
$n(X,Y,Z;V_x,V_y,V_z)$,因此不同的分布其$H$值也不同。</p>

其中,标准公式是用 $$...$$ 界定的,而行内公式是用 $...$ 界定的,MathJax遇到这样的LaTeX代码,就会自动渲染成公式。关于MathJax对LaTeX的支持,可参见 官方文档 。值得注意的是,由于 $...$ 界定符比较容易混淆(正文中可能会遇到美元符号),因此MathJax默认是使用 \(...\) 来界定行内公式的。如果需要开启 $...$ 界定符(比如说中文排版中遇到美元符号的情况极少,因此不大会混淆),可以在HTML文件中插入一段MathJax配置脚本:

<script type="text/x-mathjax-config">
    MathJax.Hub.Config({
        tex2jax: {
            inlineMath: [
                ['$','$'], //追加$...$作为行内公式界定符
                ['\\(','\\)']
            ]
        });
</script>

渲染方式和配置

MathJax提供很多种不同的配置文件,它们可以决定让MathJax解析哪种格式的公式描述语言,调用哪些插件和扩展,然后再以哪种格式渲染输出。关于配置文件,具体参见 MathJax的文档 。上面的例子中提到的 config=TeX-AMS_SVG 就是一个预置的配置文件,它接受TeX(LaTeX)输入,然后输出成SVG矢量图。

关于MathJax的输出格式,常用的一般有两种:Common HTML和SVG。前者是输出带样式的HTML代码,后者是输出SVG矢量图片,在这本书里使用的是SVG,因为我发现SVG能够最准确地还原渲染效果和一些特殊符号的样式。就网页应用来说,SVG可能会增加网页的数据量,但印刷排版我们不在意数据量,因为最终渲染成PDF之后都是一样的。

关于SVG输出,我尝试下来发现默认的配置参数有一些问题,比如SVG字体过大(原因不明)、过粗等等,我们可以在MathJax配置脚本中调整:

<script type="text/x-mathjax-config">
    MathJax.Hub.Config({
        SVG: {
            scale: 87, // 字体缩放87%,经验值
            blacker: 0, // 字体粗度0
            undefinedFamily: "SongTi" // 回退字体
        });
</script>

这里需要解释一下 undefinedFamily 参数。我们的公式中可能会出现汉字,但是MathJax TeX字体中是没有汉字的,这时就会出现字体回退,这个参数就是定义回退字体的,也就是说,当遇到MathJax TeX没有的字符(如汉字)时,就回退到宋体。具体效果是这样的:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

这样我们就确保了公式中的汉字字体和正文字体的统一。但是这里其实有一个遗留问题,当公式处于正文注释块中时,注释块本身的正文字体是楷体,但MathJax公式只能有一个回退字体,无法更改,这时就会造成公式和正文的字体不统一。好在这个问题没有造成太大的麻烦(编辑认为公式都用宋体也是可以接受的),不知道有没有更好的解决方案。

预渲染

MathJax是通过Javascript来渲染公式的,而Prince虽然号称支持Javascript,但实际上支持程度非常有限,在查询了多份文档之后,最终还是决定通过预渲染的方式来解决。所谓预渲染就是通过浏览器内核引擎先运行一遍MathJax的Javascript脚本,然后把输出结果写入一个HTML文件中,这个经过预渲染的文件中,公式就已经全都是SVG了,然后把这个文件再交给Prince渲染输出成PDF。

这里我使用了一个非常著名的WebKit引擎PhantomJS,相信熟悉前端开发的朋友都不陌生。PhantomJS可以通过编写Javascript的方式来完成很多操作,我们要完成的操作其实很简单,就是加载页面,等待MathJax渲染完成,然后把结果输出到一个HTML文件。

这里的一个trick是如何判断MathJax已经渲染完成了,我们是通过MathJax提供的一个 MathJax.Hub.Register.StartupHook() 函数注册事件回调函数来实现的。具体代码可以参见我在GitHub上发布的 示例代码

准备好脚本之后,就可以运行PhantomJS进行预渲染了:

phantomjs render_script.js source.html >prerender.html

除了MathJax之外,如果你还有其他的Javascript渲染逻辑(比如我们上面提到的在中文和英文之间自动加空格),都可以通过PhantomJS一次性做掉。关于PhantomJS的更多用法,可参见 官方文档

MathJax元素样式

预渲染之后的HTML中包含一些MathJax的公式元素,我们可以通过CSS调整这些元素的样式。其中比较重要的样式是行内公式两边的间距,因为行内公式不属于英文字母,所以我们的自动加空格脚本对这些元素不起作用,需要单独定义:

.MathJax, .MathJax_SVG {
  margin-left: 0.25em !important;
  margin-right: 0.25em !important;
}

另外,对于带公式编号的标准公式,我发现渲染出来位置不居中(不带编号的公式没有问题),造成右侧的公式编号超出了正文的右侧边界。我用Chrome的开发者工具看了一下SVG的源代码,感觉好像是SVG元素定位机制的问题。因为公式和右侧的编号加起来是一整张SVG,但公式和编号之间是一段空白,这段空白是用SVG的元素定位(translate)来实现的,这里面似乎存在误差。这本书里面我用了个简单粗暴的办法,直接调整了一下标准公式元素的左边距,我不知道有没有更好的办法,欢迎大家讨论:

.MathJax_SVG_Display {
  text-indent: -2.2em !important;
}

其他细节

上面分享了关于这本书的排版中遇到的基本问题,最后再补充一些小细节。

PDF书签

这本书送印之后,编辑告诉我出版社要求保留存档的PDF文件需要带书签,就是这样的:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

这个书签当然是可以自动生成的,其实Prince已经为 <h1> 到 <h6> 定义了默认的书签级别,相当于默认设置了下列样式:

h1 { bookmark-level: 1 }
h2 { bookmark-level: 2 }
h3 { bookmark-level: 3 }
h4 { bookmark-level: 4 }
h5 { bookmark-level: 5 }
h6 { bookmark-level: 6 }

因此只要正确使用 <h1> 到 <h6> 标签,书签就可以自动生成了。当然,有时候我们只希望 <h1> 到 <h3> 列入书签,再次级的标题不列入书签,那么我们可以取消 <h4> 到 <h6> 的书签级别:

h4, h5, h6 {
  bookmark-level: none;
}

还有一种情况是我希望自定义书签的内容,比如说版权页上面可能没有标题,但我依然想在书签里加入版权页,这时需要手动设置书签样式:

div.copyright {
  bookmark-level: 1;
  bookmark-label: "版权页";
}

最后需要提醒的是,如果需要对Prince生成的PDF文件进行后处理,比如合并或者替换页面,请务必注意保留书签信息,因为有些工具需要指定某些参数才能保留原始文件的书签信息。

防伪标记

编辑还提了一个很奇葩的需求,就是人邮社要求在正文第55页的订口印上竖排的“邮电”两个字,效果是这样的:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

这个需求应该说不难实现,我们可以通过 @page 的 ::nth() 伪元素来匹配指定的页面,然后在页面的 @left-middle 页边框中加上这两个字。这里面有两个小问题,首先 ::nth() 匹配的是实际页数,而不是 page 计数器所代表的显示页码,因此需要自行计算一个偏移量。在这本书里,开头的空白页和两页目录是不计入页码的,因此第55页的实际页数是58。其次,“邮电”这两个字是竖排的,我们可以理解为 邮\n\n电 ,不过CSS的 content 属性中换行符不是 \n , content 中的反斜杠是代表Unicode编码,而换行符的编码是 U+0A ,也就是 \A 。此外,还必须加上 white-space: pre 才能正确换行。完整代码如下:

@page main:nth(58) {
  @left-middle {
    content: "邮\A\A电";
    font-family: HeiTi;
    font-size: 0.9rem;
    white-space: pre;
    text-align: left;
    margin-left: 1em;
  }
}

着重号

着重号是一种特殊的汉字标点,它显示在文字的下方,比如这样:

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

这实际上是一个遗留问题,因为我没有找到如何实现着重号显示的方法。实际上,CSS提供了一个 text-emphasis 属性,用来显示着重号,这个属性已经被Firefox、Chrome、Safari等主流浏览器支持,但遗憾的是,Prince尚未支持这个属性。为此,我向Prince的开发团队反映了这一情况,开发团队表示已经将这一属性的支持列入了 Roadmap ,希望这一功能能够早日实现。

更多参考资料

HTML+CSS印刷排版的内容非常多,这篇文章当然无法介绍其中所有的技术细节,如果大家对这个话题有兴趣的话,可以继续参考下面一些资料:

后记

这本书得以顺利出版,应该说意味着我的这次有趣的尝试获得了成功。从这一次经历来看,尽管我无法将其与传统排版工具进行一个客观的对比(因为我没有用InDesign之类的传统工具做过排版),但我认为HTML+CSS的功能和灵活性应该足以应付大多数印刷排版的场景,如果再结合sass、less等CSS预处理器,应该还能进一步优化工作流程。

当然,这本书是一本科普读物,没有遇到计算机技术图书中的代码排版问题,也没有太多的插图,版式也比较简单,唯一比较复杂的就是数学公式。如果有机会的话,我也会考虑尝试更复杂的排版任务,有必要的话也可以专门开发一些脚本和工具,也欢迎同样有兴趣的朋友一起探讨。

实录:《周自恒:HTML+CSS 在中文排版中运用实战解析》

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

查看原文: 一本由译者自己排版的书:谈谈HTML+CSS在中文图书排版中的实际运用

  • smallladybug
  • greengoose
  • organiccat
  • bluewolf
  • smalldog
  • OnionsMerlin
  • MarjoryGiles
  • FosterConstance
需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。