
简介Manuscript 是一份围绕“手稿”主题的字体资源包面向网页设计、文档排版及平面设计学习者旨在解决在文档编辑、出版排版或视觉设计中如何选用合适字体的问题。压缩包内共包含3个文件整体大小仅45KB其中包括TTF格式的字体文件、GIF格式的预览图以及HTM格式的说明页面用户解压后即可查看字体外观、安装字体文件并通过说明页了解字体使用建议。已有293人学习浏览。通过这份资源读者可以获得一款可实际安装的手写体字形适用于标题、签名、引用或装饰性文本同时预览图与说明文档有助于快速评估其风格是否匹配特定场景节省逐字试用的时间。在数字文档处理中字体不仅影响阅读体验还承载着作品的整体观感这份素材包既能帮助初学者理解手写体与衬线、无衬线字体的差异也能让设计师快速调用符合“手稿”气质的字形提升排版效率与视觉效果。对于关注字体可读性与视觉效果的创作者而言这是一份小巧但实用的字体素材。1. Manuscript 项目解析一份中文字库从手写到落地的完整工程做中文字体设计的人都知道这是一门“慢工出细活”的手艺也是一条充满暗坑的工程链路。英文只有 26 个字母而汉字仅 GB2312 就覆盖 6763 个常用字GBK 更是多达 20902 个字符。市面上绝大多数中文手写体字库单个字形的曲线点数动辄上百个一套字库做下来文件里的锚点、路径和控制点总数轻松超过百万级。Manuscript 这个项目就是把中文字体设计从“玄学”变成“工程”的一次完整实践。它面向三类人想入行字体设计的新手需要为产品配一套手写风格 UI 字体的前端或设计师以及想优化字库渲染体积和加载性能的从业者。如果你也想跳过那些“只看效果、不讲方法”的教程直接看到字体从纸面走向屏幕的每一处细节这份资源值得你从头到尾跟着做一遍。2. 字库不是画出来就完了从笔迹到数字字形的管线与格式选型2.1 一份手写字的数字化链路从扫描到矢量的四个环节手写字库的起步不是“用钢笔工具描一遍”而是一条包含采集、预处理、矢量化、修整的完整管线。Manuscript 项目中采用的流程是先在纸上书写字符扫描成灰度图再用字体编辑软件进行矢量化。这个环节里最常见的误区是一上来就动手改锚点结果发现后面每个字形都要返工。正确的顺序是先把源图的分辨率对齐到统一标准再进入矢量化阶段。# 对扫描件做统一的灰度处理与裁切 # 需要提前安装 ImageMagick convert scan_001.tif -colorspace Gray -trim -resize 1000x1000 -gravity center -extent 1200x1200 source_001.png这里做了三个关键动作-colorspace Gray把彩色扫描转换成灰度避免后续矢量化时把纸张纹理误判成笔画-trim去掉空白边缘统一画布-resize与-extent将图片尺寸归一化到 1200x1200 的方形画布上这一步至关重要——汉字结构里有大量左右、上下结构如果每页扫描的字符位置不统一矢量化后同一个偏旁会在不同字形里出现明显的位置漂移这也就是后来排版时字重忽大忽小的根源。每批扫描件处理完毕后检查每张图的灰度直方图保证笔画的峰值灰度在相近区间再进下一步。矢量化环节Manuscript 用的是 Potrace 配合 FontForge 来处理字形轮廓。Potrace 输出的矢量路径通常带有冗余节点直接导入会导致字库文件膨胀渲染时还会出现锯齿抖动。所以矢量化之后要进入 FontForge 做一次节点清理把连续直线段上的中间锚点删除只保留曲线转折处的关键点。# 用 fontforge 脚本批量简化字形路径 fontforge -langff -c Open($1); SelectAll(); RemoveOverlap(); Simplify(1.0, removesingleton); Generate($2); source.svg output.svg这段脚本里的RemoveOverlap()用来处理路径重叠手写笔画交叉处的轮廓如果没做布尔运算字体渲染时会出现填充异常也就是肉眼可见的“破洞”。Simplify(1.0, removesingleton)的语义是删除在给定误差阈值内的冗余节点1.0 代表允许的最大偏差为 1 个字体单位。参数调小会让字形更精细、文件更大调大了字形会变粗糙。对于手写体我个人会把阈值控制在 0.8 到 1.2 之间再大就丢笔锋了。2.2 OTF 还是 TTF文件格式选型不是拍脑袋字体格式的抉择在工程上的影响是持续的。TTF 与 OTF 的差异表面上只是后缀名不同实际上涉及轮廓描述方式和渲染性能。TTF 使用二次贝塞尔曲线描述字形轮廓而 OTF指 PostScript 轮廓的 CFF 版本使用三次贝塞尔曲线。二次曲线每个锚点只能控制一段弧三次曲线则可以通过一个控制点同时影响两段弧——体现在手写字上就是笔画转折处是否顺滑。对比项TTFOTF (CFF)曲线类型二次贝塞尔三次贝塞尔同一字形锚点密度偏高偏低约 20%-30%渲染兼容性全平台老系统可能有兼容缺口压缩率一般更高尤其适合网页字体手写字体的笔画内部常常有自然的粗细变化三次曲线能更精确地还原手写时的笔锋和提按。Manuscript 项目最终选型是 OTF原因有三一是单字形锚点数较少整套字库体积更小二是 FontForge 对 CFF 轮廓的 Hinting 自动处理更稳定三是网页端通过 font-face 加载时 CFF 压缩效果更好。代价是个别老旧阅读器对 CFF 字形渲染支持不佳所以项目里同时保留一个 TTF 导出版本作为 fallback。# 从 OTF 导出兼容性更好的 TTF 版本 fontforge -langff -c Open(Manuscript.otf); Generate(Manuscript.ttf); 这一段看似简单但隐藏着一个容易翻车的点直接从 OTF 转 TTF 时字体的度量信息上升部、下降部、行距在部分平台会被重新计算导致同一字号在不同系统里行高不一致。项目里一般会在导出后额外检查OS/2表和hhea表的数值是否一致。这里建议加入一个自动化检查# 检查 OTF 与 TTF 的度量信息是否一致 python3 - EOF from fontTools.ttLib import TTFont for path in [Manuscript.otf, Manuscript.ttf]: font TTFont(path) os2 font[OS/2] hhea font[hhea] print(path, ascender:, hhea.ascent, descender:, hhea.descent, lineGap:, hhea.lineGap) EOF预期两个文件打印出的 ascender、descender 和 lineGap 数值应该一致。如果出现偏差说明转换过程中字体度量被重置需要在 FontForge 里手动锁定 Element Font Info OS/2 度量表再重新导出。这步偷懒的话后面在 iOS 和 Android 上做混排对齐时会不断怀疑人生。3. 字形构建的核心操作锚点、字重与曲线质量3.1 手写字形的曲线质量控制点越少渲染越稳手写字体的“像”与“不像”很大程度上取决于曲线的控制点排布是否合理。从 Potrace 出来的路径通常带着大量短线段虽然视觉上接近原稿但一旦缩小字号到 24px 以下这些短线段会引发严重的渲染锯齿——原因很简单屏幕像素就那么几个短线段高频抖动造成相邻像素的覆盖率反复变化。Manuscript 的字形修整思路是先保证轮廓的拓扑正确再逐字形压缩控制点数量。举个例子一个“永”字包含横、竖、撇、捺、钩五种基本笔画在扫描矢量化后基础路径可能有 120 个左右的点经过Simplify压缩到 70 个点以内再手动调整关键节点让它保留手写的“软”感而不是机械的几何感。实际操作中我会把 FontForge 的网格显示打开逐点缩放检查每个控制点的切线方向。手写体的主笔画方向通常是连贯的如果某处控制点切线与笔画走势偏离超过 30 度就会在渲染时出现“鼓包”或“塌陷”。字形修整是一个典型的“越到后面越考验耐心”的环节。项目里给每个字形定了三条硬性标准无重叠路径、无孤立节点、无大于 90 度的突兀转向。这三条检查可以通过 FontForge 的 Element Validate 自动跑一遍但“突兀转向”还是得靠人眼逐个过。# 用 fontTools 统计每个字形的控制点数 from fontTools.ttLib import TTFont font TTFont(Manuscript.otf) glyf font[glyf] counts {} for name in font.getGlyphOrder(): glyph glyf[name] if glyph.numberOfContours 0: counts[name] len(glyph.coordinates) # 输出控制点最多的 20 个字形 for name in sorted(counts, keylambda k: counts[k], reverseTrue)[:20]: print(name, counts[name])这个统计脚本的现实意义是帮你找到整字库的“体积黑洞”。一套字库里 90% 的字形控制点数在 40 到 80 之间但总有那么几个笔画特别复杂的字——比如“鬱”控制点数量可能上百。如果这类字直接放进字库加载性能和渲染性能都会明显劣化。常见的处理方式是给这些字单独提高容差再跑一次Simplify手动作微调。控制点数量和字形还原度之间的平衡是字体工程里最重要的一个权衡没有标准答案但有一个参考基准同一套字库内所有字形的控制点数量分布应该稳定。3.2 字重与笔画粗细的统一不要让“横”忽粗忽细手写字的字重控制比黑体难得多。黑体可以用统一的矩形笔画去构建字重天然一致手写体则因为笔势走向、起笔收笔的不同同一笔画在不同位置天然有粗细变化。但如果整篇字库的横画粗细相差超过 15%组合在一起就会显得“乱”阅读时眼睛需要不断重新适应字形。Manuscript 项目的处理方式是用垂直笔画和水平笔画的平均宽度作为全局基准来校准。笔画维度基准宽度单位font unit浮动范围水平笔画120±15垂直笔画128±18撇捺从 90 到 60 渐变不做全局限制钩尾部收窄至 40-50不做全局限制校准方法是写一个 FontForge 脚本遍历所有字形把水平笔画宽度低于 105 或高于 135 的节点标记出来人工核对。这个过程非常费时间但也是字库整体质感的保证。一个能用的技巧是在 FontForge 里打开两个窗口左边是正在调整的字形右边是“永”字这个参照字随时对照笔画的粗细感觉。# 辅助检查单个字形中水平笔画的平均宽度 from fontTools.pens.boundsPen import BoundsPen from fontTools.ttLib import TTFont import math font TTFont(Manuscript.otf) gs font.getGlyphSet() glyph gs[永] pen BoundsPen(gs) glyph.draw(pen) print(pen.bounds) # (xMin, yMin, xMax, yMax)这段代码拿到的是字形的外包矩形用来快速判断字形画布占用是否合理。对于汉字来说每个字形应该尽量填满 em square 内的可用区域过小会让行文显得稀疏过大则会造成字与字的挤压。一般规律是单个字形的外包矩形宽度占 em 比例不小于 85%高度占 em 比例不小于 80%但字形复杂的字可以放宽。手写体的“呼吸感”通常靠整体字距来调节字形本身的饱满度不能丢。4. 字距与排版参数让手写字库真正用得上4.1 字距调整不是玄学从度量表到视觉判断字体做完了嵌入网页或 App 后却发现“手写感”是有了但字和字之间要么挤成一团要么松散到没法读。问题不在字形而在字距。字库文件里负责字距的是hmtx表和OS/2表中的xAvgCharWidth等字段。TTF/OTF 文件的每个字形都带有一个 advance width推进宽度连字排版时正是这个宽度决定字符间的间距。手写体与印刷体在字距习惯上差别很大。印刷黑体的字形结构规整字距可以设得较小手写体的笔画末端常常有游离的笔锋和牵连线如果沿用黑体的字距数值笔锋会互相碰到观感很脏。Manuscript 项目对常用字做了逐字间距修正尤其是左右结构和上下结构的字符按结构类型分组设置不同的字距。# 批量调整整份字库的字距按字形结构类型分组 from fontTools.ttLib import TTFont font TTFont(Manuscript.otf) hmtx font[hmtx] metrics hmtx.metrics # 左右结构的字左边旁笔画末端通常更外扩增加 20 单位 left_right_chars 偏像信请你他 # 独体字保持默认推进宽度 single_chars 永人中大天 for char in left_right_chars: name font.getBestCharSet().get(char) if name: metrics[name] (metrics[name][0] 20, metrics[name][1]) for char in single_chars: name font.getBestCharSet().get(char) if name: # 独体字不做调整 pass font.save(Manuscript_adjusted.otf)这段脚本用getBestCharSet()把 Unicode 字符映射为字形名然后直接修改hmtx表的 metrics。hmtx.metrics[name]是一个元组第一个值是推进宽度第二个是左侧轴距。这里存在的陷阱是修改推进宽度后字形本身的对齐可能受到影响尤其是左右结构字中左边偏旁的左侧留白如果过小视觉上会感觉字向左倾。所以更稳妥的做法是同时调整左侧轴距而非只改推进宽度。4.2 排版参数在浏览器里的实际验证字距调完必须放到真实排版环境里验证。手写字体的渲染效果在浏览器、iOS、Android 各平台上差异很大因为各平台对字体 Hinting 和抗锯齿的处理机制不同。一个比较实用的验证方式是准备一组跨平台测试页。页面上包含三种字号28px、36px、56px都启用font-feature-settings: kern来强制开启字距调整因为部分浏览器默认不启用 kerning。代码大致是这样div stylefont-family: Manuscript, cursive; font-size: 28px; font-feature-settings: kern; 字体排印是一个对细节极度敏感的过程偏旁部首之间的空隙决定了整段文字的疏密。 /div div stylefont-family: Manuscript, cursive; font-size: 56px; font-feature-settings: kern; 字体排印是一个对细节极度敏感的过程。 /div建议在 Chrome、Safari 和 Firefox 里分别截图对比。重点看三点一是“偏旁部首之间的空隙”这句里“部”和“首”两个字的间距是否均匀二是大字号下笔锋是否出现明显的毛刺三是小字号下笔画粘连程度。手写字体在小字号下的可读性往往被高估这是最常见的认知偏差——因为设计者在 500% 放大下看字形很顺眼但缩到 16px 时笔画叠成一块。如果项目面向生产环境建议以 36px 作为基准字号来校验字距因为这是正文阅读最常见的字号区间。5. 避坑指南中文字库制作的真实翻车现场5.1 灰度扫描的“纸纹噪声”污染字形轮廓现象矢量化后的字形边缘有伸出的小刺尤其是斜撇和横折钩的位置出现微小锯齿状突起。放大后能看到这些突起在字形轮廓中形成独立的小闭环。原因扫描件未做去噪处理纸张纹理或墨迹边缘的水渍被 Potrace 识别为有效轮廓。直接在 FontForge 里手动删除这些节点效率极低漏网之鱼会在渲染时造成视觉杂质。解决在矢量化前先对灰度图做中值滤波再用 ImageMagick 的-threshold做二值化把灰度图切成干净的黑白两色。这样 Potrace 接收到的输入信息量大幅减少产出的路径几乎不会带纸纹噪声。做完二值化后务必抽查边缘如果笔画边缘出现横线断裂说明阈值设得太激进需要降低阈值或改为自适应二值化。5.2 OTF 转 TTF 后中文标点在部分安卓机型里变成乱码现象同一套字库在 iOS 上一切正常在部分安卓机型上问号、双引号等标点显示为豆腐块或空白。原因OTF 转 TTF 的过程中字体文件的 cmap 表映射出现偏移。中文标点大多位于 Unicode 区块的 U3000 到 U303F 之间转格式时如果字体编辑工具没有正确保留这个区块的映射关系标点字形就会丢失。解决导出 TTF 后用 fontTools 检查 cmap 表里标点字符对应的字形名是否正常再对照原 OTF 的映射。文本上我会额外生成一个包含所有常用中文标点的样本 HTML发布前在多台安卓设备上做一次快速目检。这个坑在一开始容易被忽视因为字体编辑器内置预览只覆盖字符集的一部分标点字符不在默认显示范围内。5.3 修改字距后“字”被截断右边缘明显缺失现象调整完hmtx表后一部分字符的右侧笔画被裁切甚至字形的最右一笔消失而在 FontForge 预览里却一切正常。原因hmtx的推进宽度调得过小导致字形实际轮廓超出 advance width 的边界。渲染引擎是按推进宽度分配绘制空间的超出部分直接被裁剪。FontForge 的预览不模拟裁剪行为所以肉眼发现不了。解决改完字距后必须重新校验每个修改过宽度的字形的 xMax 是否小于新的推进宽度。可以用一行 Python 代码检查from fontTools.ttLib import TTFont font TTFont(Manuscript_adjusted.otf) glyf font[glyf] hmtx font[hmtx] for name in font.getGlyphOrder(): glyph glyf[name] if glyph.numberOfContours 0: advance hmtx.metrics[name][0] x_max glyph.xMax if x_max advance: print(f问题字形: {name}, xMax{x_max}, advance{advance})这个问题最容易出现在笔画极多的复杂汉字里因为推进宽度和字形外包矩形的比值天然比其他字形紧张。遇到这类字形优先调整左侧轴距而不是继续加宽推进宽度避免打破整体节奏。5.4 字库 Hinting 自动生成导致大写字母笔画颤抖现象把字体放大到 48px 以上时部分笔画边缘呈现轻微的海浪状抖动越放大越明显而原本的矢量轮廓很平滑。原因TrueType 的 Hinting 指令在自动生成时对曲线路径做了额外的像素网格对齐。对于复杂的手写笔画自动 Hinting 无法准确判断哪些曲线是主笔画哪些是装饰性笔锋导致在特定字号下错误地对齐渲染时反而引入了抖动。解决OTF 格式的手写字库可以关闭自动 Hinting 指令生成依赖操作系统自身的抗锯齿处理。现代渲染引擎在小字号下的效果已经足够好强制 Hinting 带来的收益有限。具体操作是导出时在 FontForge 的导出选项里取消“自动加载 Hinting 指令”然后手动清理.t2a文件中的 Hinting 区。如果必须支持小字号建议保留 TTF 版本作为低分辨率场景的专门处理。5.5 “永”字测了很多遍但整篇字形还是粗细不均现象单字检查时没发现问题一旦排成长文视觉上明显感觉到有些字“重”、有些字“轻”反复微调算法参数依然无法解决。原因问题出在字形的“黑度”不均衡而不只是横竖笔画的宽度。左右结构的字如果左边笔画少、右边笔画多即使每个笔画宽度都符合标准视觉上依然会显得“左边轻、右边重”。手写字因为笔画的连续性这个问题比印刷体更严重。解决引入一个简单的黑度估算工具把字形渲染成位图计算黑色像素占比。占比明显偏离平均值的字形单独拿出来调整局部笔画宽度而不是整体加粗或减小。这里的经验值是左右结构字的左右两部分黑度差控制在 15% 以内视觉上才算平衡。文本中如果遇到复杂字形用这个方法逐个排查不要凭肉眼看架上数值化检查之后再调效率高很多。6. 把字库变成能交付的产物性能压缩与全字符集验证字库设计完成最后一步是把它变成工程上可用的产物。这个阶段的核心是两个动作一是压缩体积二是全字符集验证。手写字库的 OTF 原文件通常在 5MB 到 10MB 之间这对桌面应用来说可以接受但如果是网页字体必须做拆分和子集化。子集化的常见做法是按 Unicode 区块拆分常用汉字区U4E00 到 U9FFF 中按拼音频次排序的前 3500 字作为核心包高频标点、数字和拉丁字母单独成包。核心包的体积如果能压到 500KB 以内网页字体的加载体验就基本可用了。项目里用 fontTools 的subset模块做这一步# 按 Unicode 范围生成核心子集 from fontTools import subset options subset.Options() options.flavor woff2 # 输出 woff2 格式 options.layout_features [*] options.name_IDs [1, 2, 3, 4] subsetter subset.Subsetter(options) font TTFont(Manuscript.otf) # 只保留中文字符、ASCII 和常见中文标点 chars [chr(c) for c in range(0x20, 0x7F)] # ASCII chars [chr(c) for c in range(0x4E00, 0x9FBF)] # 基本汉字区 subsetter.populate(text.join(chars)) subsetter.subset(font) font.save(Manuscript_partial.woff2)这段代码的关键参数有两个flavorwoff2让输出使用 WOFF2 压缩通常比 WOFF 再小 30% 左右populate接收的字符集合决定最终产物里保留哪些字形。如果只取常用 3500 字字符范围要按实际频次表过滤不能直接用整个汉字区否则子集化和不子集化没有区别。压缩完成后用woff2命令行工具检查最终文件大小如果超过 500KB再检查是否混入了不必要的GPOS表很多字库的GPOS表体积占比惊人。全字符集验证这个环节不少从业者会跳过但它是“交付底气和用户口碑”的关键。我的习惯是生成一份测试 HTML把字库里的每一个字形都渲染出来分为三块汉字区、标点区、英文和数字区并逐一写入一个图形映射表。渲染成图片后写一个脚本对比字形渲染前后的视觉完整性具体指标是笔画的连通性变化、是否有字形渲染缺失。脚本跑完后再人工抽查 10% 的页面确认字形结构没有奇怪的变形。从那以后我每次做完一套中文字库都会强制走一遍全字符集渲染检查生成一个 18px、36px、72px 三档字号截图集哪怕只是临时给团队内部使用的版本也不例外。那次在交付后才发现有 300 多个字形有问题白白损失了用户信任从此不敢再省这一步。专业的字库制作不是依赖滤镜和模板的有时觉得可以靠机器全自动打磨的事多数环节仍然需要设计师逐字审视但把工程流程标准化后每个字形的检查速度和整体出品质量的稳定性都会上一个台阶。这套方法做下来至少能让整套流程的时间预期缩掉三分之一。希望帮到你。本文还有配套的精品资源点击获取