开源排版引擎 · v1.10.0

面向Web的可编程排版引擎

Postext读入语义化的Markdown,套用延续数百年的书刊排版规则——Knuth-Plass两端对齐、各栏齐底、各得其所的浮动体——排出出版级的页面,输出为HTML、Canvas或可直接付印的PDF。

每一页都在你的浏览器中计算得出

章 1 · 为什么选择Postext

书刊排版, 为Web计算

印刷排版用了五个世纪学会怎样排一页,浏览器学会的是怎样布局一个界面。Postext把前者带给后者。

CSS提供响应式布局:flexbox、grid、多栏。但它不能让各栏齐底,不能让标题和它的段落排在一起,不能避免段首孤行和段末孤行,也不能把图浮动到下一栏的栏首。这些是编辑层面的决定,经过几个世纪的印刷实践才打磨成型。

Postext填补了这个空缺。给它语义化的Markdown和一个配置对象,它借助@chenglou/pretext测量文字,速度比DOM快300–600倍;然后计算出完整的页面几何——每一行、每个浮动体和每条题注——再通过HTML、Canvas或可直接付印的PDF后端渲染出来。

输入Markdown

:::part{number="I" title="基础"
  palette="band=#2b4acb"}
:::

# 为什么需要Postext {lead="印刷排版
用了五个世纪……"}

Postext是一个**开源排版引擎**,
把印刷排版的手艺带到网页上,
如:ref{id="fig-flow"}所示。

:::callout{type="try"}
改一个词,页面就会重新排版。
:::

输出页面

图 1.1 同一段Markdown,经过引擎前后的样子:一个章首页、两个齐底的栏,以及一个浮动到栏首的图。

数字一览

文字测量比DOM快这么多倍,所以整本书随着输入实时重排
300–600×文字测量比DOM快这么多倍,所以整本书随着输入实时重排
个渲染器共用一个版面:HTML、Canvas和可直接付印的PDF
3个渲染器共用一个版面:HTML、Canvas和可直接付印的PDF
带标签的无障碍PDF输出,含书签和CMYK颜色
PDF/UA带标签的无障碍PDF输出,含书签和CMYK颜色
台服务器:引擎完全在浏览器中运行,采用MIT许可证
0台服务器:引擎完全在浏览器中运行,采用MIT许可证

章 2 · 功能

印刷排版师早就懂得的事

页面需要、而CSS从未打算替你决定的六件事,由引擎计算,每次结果都一样。

Postext指南的每一页——封面、目录、篇章分隔页,全部都算上——都在你阅读时由Postext在你的浏览器中排出。
— 内置指南的版本说明

作品展示

真正的书,在你的浏览器中排版

一本教材、一本杂志、一部文学作品、一本展览图录、一部120回的中文古典小说。任选一本在沙盒中打开,改一条规则,看每一页随之重排。

章 3 · 工作流程

三步得到出版级版面

内容写在Markdown里,设计写在配置对象里,调用一个函数就得到页面。

  1. 3.1

    编写语义化Markdown

    用增强Markdown撰写内容:标题、列表、表格、数学公式、标注框,以及指向图的:ref交叉引用。不用操心版面,只管内容。

  2. 3.2

    配置排版规则

    在PostextConfig对象中定义页面尺寸、分栏、字体排印、标题设计、书眉和浮动体位置——也可以在沙盒中可视化地设计。

  3. 3.3

    渲染为HTML、Canvas或PDF

    调用buildDocument,得到完整的版面几何,再用HTML、Canvas或可直接付印的PDF后端渲染。

API

一个函数,完全掌控

用你的内容和一个配置对象调用buildDocument,得到完整的页面几何:用renderPage把任意一页绘制成图片,或用renderToPdf把整本书排成可直接付印的PDF。

book.ts
import { buildDocument, renderPage } from "postext";
import { renderToPdf } from "postext-pdf";

const doc = buildDocument(
  { markdown },
  {
    page: { sizePreset: "21x28" },
    layout: { layoutType: "double" },
    bodyText: {
      fontSize: { value: 12, unit: "pt" },
      textAlign: "justify",
      hyphenation: { enabled: true, locale: "en-us" },
    },
  }
);

// 每一页都绘制成图片…
const cover = renderPage(doc.pages[0], doc);
cover.toBlob((png) => save(png, "cover.png"));

// …或把整本书输出为可直接付印的PDF
// fontProvider:按字族和字重提供TTF字节(参见文档)
const pdf = await renderToPdf(doc, { fontProvider });

章 4 · 开放格式

一个文件,整本书

.postext文件包含了文档在任何机器上重新排版所需的一切:一种开放、可编辑、任何人都能采用的格式。

多数出版格式会把一个项目拆得七零八落。LaTeX的构建依赖某一台机器上安装的字体和宏包,主流排版软件的原生文件链接着存放在文件之外的图片和字体。项目一挪地方,就会缺东西。我们自己用LaTeX时就碰到过这些限制。

.postext就是一个普通的ZIP压缩包,任何人都能打开、阅读和编辑:一个JSON清单、每章一个Markdown文件,以及字体文件和图片本身。沙盒可以导出它,postext-port代理技能可以写出它,postext/bundle可以在任何程序中打开它,重新排出同样的页面。这种格式有完整的文档,采用MIT许可证,任何工具都可以读写。

postext-guide.postext里面有什么
  1. 4.1preset.json

    配置. 页面、网格、字体、标题和段落样式、颜色和书眉,都是可读的JSON。

  2. 4.2chapters/<lang>/*.md

    章. 每章一个Markdown文件:这里有12章,共2种语言。

  3. 4.3fonts/*.woff2

    字体. 字体文件本身,而不只是字体名称:Lora、Fraunces、Geist和Bricolage Grotesque的18个文件。

  4. 4.4preset.json › resources

    资源. 正文引用的22个图和表的题注、编号、位置和表格内容。

  5. 4.5resources/*.svg

    图片. 图稿本身,SVG或位图,包括每种语言的版本:32个文件。

图 4.1 本指南自己的.postext,496 KB:书需要的一切都在里面,不必额外获取任何东西。

试试看

打开指南,得到它的页面

沙盒的第一个示例,即Postext指南,就是一个.postext文件。在浏览器中打开它,把每一页都变成图片:它的字体、图和表都来自这个文件本身。

下载指南postext-guide.postext · 496 KB
open-guide.js
import { openBundle, loadBundleFonts, registerBundleImages,
  buildBundle, renderPageToCanvas } from "postext";

// 沙盒中的指南:配置、各章、字体和图片
const file = await fetch("postext-guide.postext").then((r) => r.blob());
const book = await openBundle(file, { locale: "en" });

await loadBundleFonts(book);      // 它自带的字体
await registerBundleImages(book); // 它自带的图

// 每一章的每一页,都绘制成图片
for (const doc of buildBundle(book))
  for (const page of doc.pages) {
    const canvas = document.createElement("canvas");
    renderPageToCanvas(page, doc, canvas, { scale: 0.5 });
    document.body.append(canvas);
  }

安装

一分钟内上手

只需一个包,无需服务器,没有原生依赖。需要印刷时再加上postext-pdf。

quick-start.ts
// 快速开始
import { buildDocument, renderPage } from "postext";
import { renderToPdf } from "postext-pdf";

const doc = buildDocument({
  markdown: "# Hello World\n\nYour content here.",
});

// 第一页,绘制在Canvas上
document.body.append(renderPage(doc.pages[0], doc));

// 整个文档输出为PDF
const pdf = await renderToPdf(doc, { fontProvider });