◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。
如果你对许多从事网页开发的人提起打印样式,他们第一反映会想到使用打印CSS。当网页文档需要被打印时,我们已经习惯于创建一个打印样式表。这些样式表确保打印版本清晰并且保证用户不会打印出巨幅图像。然而,CSS也被用来排版图书,商品目录以及宣传册 - 这些内容在以前可能从来没有被设计成网页。
在这篇文章里,我们将探索一些已有的CSS模块,它们不是在网页浏览器中使用,而是为了解决打印和分页的问题。我将解释选择器,属性和值的作用。最后我会提供一个实际例子,你可以把它作为自己试验的基础。对于这个例子,我们需要支持这些特殊CSS。我正在用的是Prince,它是一个商业软件。然而,Prince有针对非商业使用的免费版本,是实验这些例子的好工具。
非要用HTML去维护和用CSS去排版那些不是特意为网页设计的内容似乎有点奇怪。但是当你意识到流行的电子阅读格式例如EPUB和MOBI在底层是由HTML和CSS组成时就没那么奇怪了。另外,即便手稿或目录没有被整个发布到网页上,一些类似的东西可能会。HTML成为了一个方便的格式化标准,比起把所有东西都放在一个Word文档或者传统的桌面出版包中更容易处理。
最大的区别和观念的转变在于印刷文档需要一个固定大小的页面模型。在网页上我们不断被提醒我们不知道视窗尺寸,在印刷中每个固定尺寸的页面承载了我们所有的东西。因为页面尺寸固定,我们需要把文档考虑成网页的集合,即分页媒体,而不是连续媒体即网页。
分页媒体的内容在网页上毫无意义。例如,你需要能生成页码,给章节标题添加边距,适当划分内容并保证图形和说明不脱钩。也许你要创建交叉参考和脚注,索引和文档目录。你可以把文档引入桌面发布包并且手动创建这些内容,然而,当你下次更新复本时需要重复这些工作。这时候CSS就起到作用,它的规则是为创建分页媒体而设计。
因为规则是为分页媒体而设计,在这篇文章里我们不考虑浏览器兼容性 - 这没多大意义。稍后,我们会看一个被设计成使用特定规则把HTML和CSS转换成PDF的用户代理。
你已知的大多数CSS在印刷品排版中有效。在印刷品中比较特别的是,我们有“CSS分页媒体模块”和“CSS分页媒体模块生成内容”规则。让我们看一下这些规则如何生效。
@page
规则允许你指定页面盒子的许多方面。例如,你想要指定页面尺寸。下面这条规则指定默认页面尺寸是5.5*8.5
英尺。如果你想打印一本书,也许通过按需打印,找到可用尺寸很重要。
@page { size: 5.5in 8.5in; }
除了可以用长度值声明尺寸,你还可以使用纸质尺寸关键字,例如"A4"或““legal”。
@page { size: A4; }
你也可以使用关键字来指定页面方向 - ““portrait””或“landscape”。
@page { size: A4 landscape; }
进一步讨论前,我们需要理解分页媒体运行的页面模块,因为它和运行在屏幕上的内容表现上有一些不同。
页面模块定义了一块页面区域和16个环绕的边距盒。你可以控制页面区域的尺寸以及页面区域边界与页面自身结尾之间边距的尺寸。规范中的表格很好得解释了如何定义这些盒子尺寸。
页面区域是页面上一块页面内容流动的空间。当超出了它的容纳范围,就会创建另一个页面。边界盒只在CSS生成的内容上使用。
页面模块的另一方面是它定义了伪类选择器来选择文档的偶数或奇数页。如果你看一下手边的任何印刷书,你会发现边距尺寸和边距内容在左和右边边距上是不一样的。
我们可以用这些选择器给页面定义不同的边距尺寸。
@page :left { margin-left: 3cm; } @page :right { margin-left: 4cm; }
规则还定义了两个伪类选择器。:first
选择器选中是文档的第一页。
@page :first { }
:blank
伪类选择器选中任何“故意左侧留白”的页面。要添加这样的文字,我们可以使用目标是边距盒顶部中心的生成内容。
@page :blank { @top-center { content: "This page is intentionally left blank." } }
在上个例子中,我们使用CSS生成内容在边距盒的顶部中心添加文字。你会发现,生成内容在创建书的时候至关重要。这是把东西添加到边距盒上的唯一方式。例如,如果我们想把书名添加到奇数页边距盒的左下角,我们会用生成内容来实现。
@page:right{ @bottom-left { margin: 10pt 0 30pt 0; border-top: .25pt solid #666; content: "My book"; font-size: 9pt; color: #333; } }
另外“页面媒体”部分特殊之处在于如何控制分页。像描述的那样,一旦内容充满页面区域,它会移到新页面上。如果标题正好才写到页面上,可能页面会以标题结束,相关内容在下一页开始。在印刷书中,你会试图避免这种情况。其他你想要避免断开的地方还有表格中间以及图形和对应的说明间。
通常用h1
标题来开始一本书的新章节。要强制标题总是处于页面开头,把page-break-before
设置为always
。
h1 { page-break-before: always; }
为了避免标题后立即断页,使用page-break-after
。
h1, h2, h3, h4, h5 { page-break-after: avoid; }
为了避免断开图像和表格,使用page-break-inside
属性
table, figure { page-break-inside: avoid; }
书总是跟数字相关-页,章节,甚至图像。我们可以通过CSS添加这些数字,避免由于我们打算在章节中添加一个新的图像而把所有东西重新编号。我们使用CSS计数器来实现。
首先开始的地方显然是页码。CSS提供了预定义页面计数器;它从1开始并且每新的一页加1。在你的样式表中,你会使用这些计数作为生成内容的值,把页数放到边距盒的其中之一。在下面的例子中,我们把页码放在奇数页的右下角和偶数页的左下角。
@page:right{ @bottom-right { content: counter(page); } } @page:left{ @bottom-left { content: counter(page); } }
我们还创建了一个叫做pages
的计数器。这个计数器的值总是文档总页数。如果你想输出“120页中的第3页”,你可以像下面那样做。
@page:left{ @bottom-left { content: "Page " counter(page) " of " counter(pages); } }
你可以创建自己命名的计数器和增量并且按需要重置它们。要创建一个计数器,使用counter-reset
属性,通过counter-increment
增加。下面的CSS规则会为章节创建一个叫chapternum
的计数器并且每出现h1
增加-在这本书中作为每个章节的开始。随后我们使用计数器的值在生成内容中章节的实际标题前添加章数和时间。
body { counter-reset: chapternum; } h1.chapter:before { counter-increment: chapternum; content: counter(chapternum) ". "; }
书中图像的计数我们也可以用同样方法。通常给图像计数的方法是使用chapternum.figurenum
。所以“Figure 3-2”代表第三章第二幅图。在h1
里,我们可以重置figurenum
让它每章都从1开始。
body { counter-reset: chapternum figurenum; } h1 { counter-reset: figurenum; } h1.title:before { counter-increment: chapternum; content: counter(chapternum) ". "; } figcaption:before { counter-increment: figurenum; content: counter(chapternum) "-" counter(figurenum) ". "; }
让我们再看一下印刷图书。当你翻阅一章,你会看到章节标题印在偶数或奇数页上。也许听起来很奇怪,“页面媒体生成内容”规范让我们通过CSS来实现。
我们在想要获取内容的选择器中使用string-set
属性来实现,这将成为h1
。string-set
的值是你想给这段内容取得名字然后使用content()
。随后你可以用string()
作为生成内容输出。
h1 { string-set: doctitle content(); } @page :right { @top-right { content: string(doctitle); margin: 30pt 0 10pt 0; font-size: 8pt; } }
当你的页面媒体生成后,每当出现h1
,内容被写入doctitle
然后输出到右侧页的右上边距盒中,只有当另一个h1
出现才发生改变。
脚注是“CSS页面媒体模型生成内容”规范的一部分。脚注原理是在脚注行内添加文本,包裹在HTML标签中(很可能是span
),用一个类定义它为脚注。当页面生成时,这个“脚注元素”的内容会被移除然后转换成脚注。
在CSS文件中,使用脚注的float
属性值来给脚注类创建规则。
.fn { float: footnote; }
在你的文档中,使用这个类来包裹任何脚注文本。
<p>Footnotes<span class="footnotes">Footnotes and notes placed in the footer of a document to reference the text. The footnote will be removed from the flow when the page is created.</span> are useful in books and printed documents.</p>著作权归作者所有。
脚注有一个预定义计数器表现和页面计数器一样。通常,你想让计数器每次fn
类出现时增加1并且在每一章开始时被重置。
.fn { counter-increment: footnote; } h1 { counter-reset: footnote; }
脚注的不同部分可以用CSS伪元素来选中。footnote-call
是文本中指示脚注的数值锚。它使用脚注计数器的值作为生成的内容。
.fn::footnote-call { content: counter(footnote); font-size: 9pt; vertical-align: super; line-height: none; }
footnote-marker
是放置在文档底部脚注前的一个数值标记。原理和CSS中生成有序列表的数值类似。
.fn::footnote-marker { font-weight: bold; }
脚注处于边界内,在页面中一块特殊区域叫做@footnotes
。你可以用下面的方式选中并给这块区域赋予样式。
page { @footnotes { border-top: 1pt solid black; } }
在我们继续谈论把我们所学运用其中的例子之前,让我们看一下交叉引用。在网页中,我们通过添加链接交叉引用。在书和其他打印文档中,通常依靠页码寻找引用。因为页码也许会随着书的不同版本打印形式的变化而改变 - 通过CSS实现让我们不需要检查和改变所有数字。
我们用另一个新属性,target-counter
,添加这些数字。在文档中创建链接时,赋予它们href
,值为你想要标记的文档中的元素的ID。此外,增加一个类来识别它们是交叉引用,而不是一个外部链接;我用xref
。
<a class="xref" href="#ch1" title="Chapter 1">Chapter 1</a>著作权归作者所有。
然后,在这个链接后,再一次使用生成内容输出(page x)
,x
是ID在书中位置的数值。
a.xref:after { content: " (page " target-counter(attr(href, url), page) ")"; }
当我们为实际例子创建目录时会在实践中查看这个技术。
我们分别看了很多不同的属性。一旦你通过创建一本书把它们放到一起时它们会更有意义。
真正用CSS来创建一本书,你需要一个支持的用户代理。现在,很少有东西很好得实现这些规范;目前最好用的是Prince。Prince的独立商业许可很贵,但是你可以在非商业项目中免费使用Prince。这意味着如果你只是想尝试这些技术,你可以用。另外,如果你确实有使用这项技术的非商业需求,你也许可以用Prince来排版这些书。
腾堡项目里找到我最喜欢的书,Harrison Weri的Our Cats中提取了一些段落,我选这本书因为我喜欢猫并且它有图片和脚注可以用来验证排版。
你可以在GitHub上找到我用的文件,加上一份生成的PDF。如果你想用CSS实验并且自己编译这本书,你需要下载和安装Prince。Prince是Mac下的命令行工具,虽然也有Windows GUI,我使用命令行因为它真的很简单。
使用命令窗口,切换到你书的目录或者你从GitHub上下载文件的路径。
cd /Users/username/smashing-css-books
现在,运行Prince:
prince -s pdf-styles.css book.html builds/book.pdf
这会在builds
目录下创建一个叫book.pdf
的PDF文件。现在,如果你对CSS或HTML做了任何更改,你可以运行Prince来看一下发生了什么变化。
我整本“书”是在一个HTML文档中编译的。在Prince中编译文档是可能的,但我发现只有在处理一个比较大的文档时才比较方便。在每个由h1
开始的章节前,我都放了一个div包含了封面图,然后是书的目录。
目录链接到每个章节h1
标题的ID。
<!DOCTYPE html> <html dir="ltr" lang="en-US"> <head> <meta charset="utf-8" /> <title>Our Cats and All About Them</title> <meta name="author" content="Harrison Weir"/> <meta name="subject" content="Cats. Their Varieties, Habits and Management; and for show, the Standard of Excellence and Beauty"/> <meta name="keywords" content="cats,feline"/> <meta name="date" content="2014-12-05"/> </head> <body> <div class="frontcover"> </div> <div class="contents"> <h1>Extracts from Our Cats and All About Them by Harrison Weir</h1> <ul class="toc"> <li><a href="#ch1">The First Cat Show</a></li> <li><a href="#ch2">Trained Cats</a></li> <li><a href="#ch3">General Management</a></li> <li><a href="#ch4">Superstition and Witchcraft</a></li> </ul> </div> <h1 id="ch1" class="chapter">The First Cat Show</h1> <p>… </p> <h1 id="ch2" class="chapter">Trained Cats</h1> <p>… </p> <h1 id="ch3" class="chapter">General Management</h1> <p>… </p> <h1 id="ch4" class="chapter">Superstition and Witchcraft</h1> <p>… </p> </body> </html>
接下来的CSS中使用了我们目前为止讨论的所有东西。一开始,我们需要用@page
规则设定书的尺寸。然后使用:first
伪类选择器来移除第1页的边距,因为这一页会有封面图。
@page { size: 5.5in 8.5in; margin: 70pt 60pt 70pt; } @page:first { size: 5.5in 8.5in; margin: 0; }
然后我们处理封面的图片,确保它覆盖了整个页面区域。
div.frontcover { page: cover; content: url("images/cover.png"); width: 100%; height: 100%; }
下一步,我们处理偶数和奇数页的细节,使用:right
和:left
传递伪类选择器。
奇数传递会让书的标题处于边框盒左下方,页数在右下方,章节标题在右上方。章节标题随后在样式表中用string-set
设置。
@page:right{ @bottom-left { margin: 10pt 0 30pt 0; border-top: .25pt solid #666; content: "Our Cats"; font-size: 9pt; color: #333; } @bottom-right { margin: 10pt 0 30pt 0; border-top: .25pt solid #666; content: counter(page); font-size: 9pt; } @top-right { content: string(doctitle); margin: 30pt 0 10pt 0; font-size: 9pt; color: #333; } }
偶数传递让书的标题处于右下方以及页数处于左下方。
@page:left { @bottom-right { margin: 10pt 0 30pt 0; border-top: .25pt solid #666; content: "Our Cats"; font-size: 9pt; color: #333; } @bottom-left { margin: 10pt 0 30pt 0; border-top: .25pt solid #666; content: counter(page); font-size: 9pt; } }
对于第一页,包含了封面图,我们通过设置为normal
来确保没有生成内容。
@page:first { @bottom-right { content: normal; margin: 0; } @bottom-left { content: normal; margin: 0; } }
样式表接下来的一部分处理计数器。除了预设页面计数器外,我们给章节和图解定义计数器。
/* Reset chapter and figure counters on the body */ body { counter-reset: chapternum figurenum; font-family: "Trebuchet MS", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Tahoma, sans-serif; line-height: 1.5; font-size: 11pt; } /* Get the title of the current chapter, which will be the content of the h1. Reset figure counter because figures start from 1 in each chapter. */ h1 { string-set: doctitle content(); page-break-before: always; counter-reset: figurenum; counter-reset: footnote; line-height: 1.3; } /* Increment chapter counter */ h1.chapter:before { counter-increment: chapternum; content: counter(chapternum) ". "; } /* Increment and display figure counter */ figcaption:before { counter-increment: figurenum; content: counter(chapternum) "-" counter(figurenum) ". "; }
现在在标题后章节有了自己的数字。图解也显示出它们的数字。
我们像之前解释的那样创建脚注,给脚注引用添加上标。
.fn { float: footnote; } .fn { counter-increment: footnote; } .fn::footnote-call { content: counter(footnote); font-size: 9pt; vertical-align: super; line-height: none; } .fn::footnote-marker { font-weight: bold; } @page { @footnotes { border-top: 0.6pt solid black; padding-top: 8pt; } }
我们添加一些规则来控制页面的中断。你需要非常小心避免过度。如果你的书有许多表格和图像,添加许多细节规则可能导致书中有许多缺口。实验和测试会表明你可以怎样用程序控制中断。我总结了下面规则作为一个良好的开端。
记住这只是用户代理的建议。在有些情况,如果表格不适应页面要保持表格不中断是不可能的!
h1, h2, h3, h4, h5 { font-weight: bold; page-break-after: avoid; page-break-inside:avoid; } h1+p, h2+p, h3+p { page-break-before: avoid; } table, figure { page-break-inside: avoid; }
最后,我们给目录赋予样式,这里我们使用有趣的窍门。当描述交叉引用时,我解释了如何使用target-counter
来显示带ID的页码。这也是我们要对我们的目录做的。下面的规则在目录的每一章的链接后放置页码。
ul.toc a::after { content: target-counter(attr(href), page); }
然而,通常在书中,你会使用前导字符点(.
)来组织所有的页码的右边距。让人惊讶的是,CSS提供了我们一个方式来实现这个,通过在生成内容中数字前添加leader()
。
ul.toc a::after { content: leader('.') target-counter(attr(href), page); }
现在我们有了一个完整的样式表来构建我们的书。我避免花费很多时间在排版上,集中精力在创作书的细节上。然而,从这个角度看,你可以实验并且添加你自己的样式来创建独特的书籍设计。
记住这些技术不仅仅是为了书。你可以用它们从已经给顾客开发的网站的HTML中生成打印和PDF版本产品目录。或者你可以从网页内容中创建传单和小册子。
如果你想用Prince从网页中创建PDF文档,DocRaptor是一个好选择。这项服务通过API调用Prince。你可以通过API发送文档并且接收PDF-允许用户转换成PDF下载内容。所有我们在这篇文章中讨论的东西通过DocRaptor整合的API已经都可以实现。
即使你现在没有生成PDF的需求,这仍然是CSS中一个迷人的领域 - 这是一个隐秘的实用技能,当需求出现时你知道这是可以实现的。
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。