alitrack

我给 TypePress 加了手绘风格

上周末我干了件事——让 PDF 里的架构图看起来像手画的。

TypePress 是我做的一个 HTML 转 PDF 工具,Rust 写的,不依赖浏览器。之前它产出的 PDF 都是规整的矢量线条,标准、干净,但有点太干净了。那种"手稿感"是没有的。

所以我就在想:能不能让输出带一点人味?

● ● ●

两个方向,一个坑

手绘效果有两类路数:

一是 Canvas 方案。Rough.js 这个不到 10KB 的库,能把任何 SVG 图形画成手绘抖动风格。Excalidraw 就是用它做渲染的。问题是——TypePress 没有 JS 运行时。Fulgur(渲染引擎)走的是 Blitz → Taffy → Krilla 这条纯 Rust 链路,Canvas API 不存在。

二是纯 CSS。这个方向跟 TypePress 天然兼容。我试了六种手法:

不对称圆角。border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px,极端不对称的 X/Y 轴圆角,模拟"画歪了"的框线。

双层伪元素。::before 画一个虚线框往外偏 5px 再旋转 0.4°,::after 画一个细框旋转 0.3°,叠在一起就是"画了两笔没对齐"的质感。

荧光笔高亮。linear-gradient(120deg, transparent 0 15%, rgba(255,245,157,0.85) 15% 85%, transparent 85%),120° 倾斜模拟笔尖斜擦。

波浪下划线。原生 CSS text-decoration: underline wavy,不用任何额外代码。

笔记本横线。两层 repeating-linear-gradient,横线 28px 间隔 + 左侧红线 42px。

便签纸折角。box-shadow 加 ::after 的 border trick 画右下三角形。

这六种手法全是纯 CSS,TypePress 直接渲染,PDF 里连边框的抖动都保持原样。

Image

● ● ●

SVG 的坑

CSS 能搞定装饰,但架构图这种需要画图形的场景,CSS 就不够了。得用 Rough.js 先生成 SVG,再转成 PNG 嵌进 HTML。

思路是:Rough.js 在 Node.js 里运行,输出 SVG 字符串 → 写文件 → 用 resvg 转成 PNG → 把 PNG 嵌进 HTML → TypePress 渲染 PDF。

听起来顺,实际踩了两个坑。

第一坑:inline SVG 不能用。 Fulgur 的 extract_inline_svg_tree 函数返回的是 None——usvg 0.45/0.46 版本冲突,这个功能在 TypePress 的 fulgur fork 里被 stub 了。你把 <code><svg></code> 标签写进 HTML,它当空气。

第二坑:图片注册。 TypePress 的 Mermaid 管线能把生成的 PNG 注册到 AssetBundle 里,让 <code><img src="txp-mermaid-0"></code> 能正常解析。但我没有对应的 CLI 入口——用户没法从命令行往 AssetBundle 加图。

所以我在 TypePress 里加了个 -i 标志:

typepress doc.html \
  -i sketch-arch=architecture.png \
  -i sketch-flow=flowchart.png \
  --autofit -o output.pdf

-i name=path 把 PNG 文件注册到 AssetBundle,HTML 里的 <code><img src="name"></code> 就能渲染了。跟 Mermaid 用的是同一个 bundle.add_image 接口,只是暴露到了 CLI。

● ● ●

字体是灵魂

手绘风格的核心其实是字体。Caveat(英文流畅手写)和马山正(中文毛笔手写)这两个 Google Fonts 是主力。TypePress 的 @font-face 支持会自动下载远程字体并嵌入 PDF,子集化后文件也很小。

但 resvg 需要系统字体。我把 Caveat 和马山正的 ttf 文件放到 ~/.local/share/fonts/,fc-cache -f 刷新缓存,就能找到。SVG 里写 font="Caveat",不能写 font="Caveat, cursive"——resvg 的 usvg::text 模块是根据精确名称匹配的,不支持 CSS fallback 语法。

● ● ●

封面也是手绘的

这篇文章的封面就是用这个管线生成的。一个 900×383 的 HTML,用不对称圆角做边框、马山正写标题、Caveat 写副标题,TypePress 渲染成 PDF,PyMuPDF 转成 PNG。全程零浏览器。

● ● ●

整套管线

JSON spec → Rough.js SVG → resvg PNG → TypePress PDF

我把这个管线包成了一个 skill,放在 Hermes Agent 的 skill 仓库里。里面包含了:

  • rough-svg-gen.mjs:Rough.js 生成手绘 SVG 的 Node.js 脚本
  • hand-drawn-pdf.sh:一键管线(JSON → PDF)
  • 纯 CSS 手绘技巧参考(六种手法,可直接复制粘贴)
  • JSON 图表示例格式文档

支持的形状:矩形、圆、椭圆、线段、多边形、曲线、折线,填充模式有 hachure(斜线)、solid、cross-hatch、dots、zigzag 等。

random seed 是可控的——同一个 seed 多次生成,SVG 完全一致。所以手绘感是"看起来随机"的,不是真的随机。

Image

● ● ●

接下来

目前管线的主要限制是——Rough.js 需要通过 Node.js 运行,不在 TypePress 里。理论上可以把 Rough.js 的核心算法移植到 Rust(几个千行的 s() 函数),做成 --rough 内置功能。但那是一个独立的大活儿,暂时不在计划里。

如果你也用 TypePress,cargo install typepress 更新到最新版就能用 -i 标志。完整的手绘 skill 代码(含 Rough.js 脚本、CSS 参考、示例 spec)已开源在 GitHub:

github.com/alitrack/labs/tree/master/hand-drawn-pdf

试试看看。