ekp/readme_zh.md
2026-01-24 22:46:48 +08:00

5.9 KiB
Raw Blame History

Emacs-KP: Knuth-Plass 排版算法 Emacs 实现

Emacs-kp 实现了 Knuth-Plass 最优断行算法,并扩展支持 CJK中日韩与拉丁文混合排版。

演示

ekp-demo

算法原理

Knuth-Plass 算法

本算法基于 Donald Knuth 和 Michael Plass 于 1981 年发表的经典论文 "Breaking Paragraphs into Lines"。与大多数文本编辑器使用的贪心断行不同K-P 算法同时考虑所有可能的断点,寻找全局最优解。

核心概念

1. Box盒子、Glue胶水、Penalty惩罚

文本被建模为三种元素的序列:

  • Box:不可分割的内容(字符、单词),具有固定宽度
  • Glue:弹性空白,具有理想宽度、可拉伸量、可压缩量
  • Penalty:在特定位置断行的代价(如连字符断词)
┌─────┐      ┌─────┐      ┌─────┐
│ Box │─Glue─│ Box │─Glue─│ Box │
└─────┘      └─────┘      └─────┘
 单词     (弹性空白)    单词

2. Badness劣度衡量行的质量

每行的质量由 glue 需要拉伸/压缩的程度来衡量:

            ⎧ 0                           若 adjustment = 0
badness =   ⎨ ∞                           若无法容纳
            ⎩ 100 × |adjustment/flexibility|³
  • adjustment = 目标宽度 - 自然宽度
  • flexibility = 可拉伸总量(拉伸时)或可压缩总量(压缩时)

3. Demerits缺陷值评估断行序列

Demerits 综合 badness 和 penalty 来评估整个段落的排版质量:

demerits = (line_penalty + badness)² + penalty² + fitness_penalty

其中:

  • line_penalty每行的基础代价默认10
  • penalty断点特定代价连字符50
  • fitness_penalty:相邻行松紧度差异过大时的额外代价

4. Fitness Classes适应度等级

行按松紧度分类,确保视觉一致性:

  • 等级 0紧凑显著压缩
  • 等级 1正常接近理想
  • 等级 2宽松拉伸
  • 等级 3非常宽松显著拉伸

相邻行等级差超过 1 会产生额外惩罚。

5. 动态规划

算法使用 DP 在所有有效断点中寻找最小 demerits 路径:

dp[k] = min over all valid i < k {
    dp[i] + demerits(从 i 到 k 的行)
}

时间复杂度O(n²)n = 潜在断点数量。

CJK 扩展

Emacs-kp 为 CJK 文本扩展了原算法:

  1. 字符级断行CJK 文本可在任意字符间断行
  2. 混合间距Latin-Latin、Latin-CJK、CJK-CJK 三种 glue 类型
  3. 标点处理CJK 标点附着于相邻字符

连字符断词

拉丁语单词断词使用 Frank Liang 的算法TeX 的断词算法):

  • 基于模式匹配的优先级方法
  • 特定语言的词典en_US、de_DE、fr 等)
  • 可配置断点前后的最小字符数

局限

目前仅支持 CJK 与一种拉丁语言的混合排版。不支持多种拉丁语言混排,因为系统无法可靠判断单词属于哪种语言以进行断词。

用法

配置项

ekp-latin-lang:用于断词的主要拉丁语言(默认:"en_US")。 支持的语言见 dictionaries/ 目录。

ekp-param-set:配置间距参数(单位:像素):

参数 说明
ekp-lws-ideal-pixel 拉丁单词间的理想间距
ekp-lws-stretch-pixel 拉丁单词间的最大拉伸量
ekp-lws-shrink-pixel 拉丁单词间的最大压缩量
ekp-mws-ideal-pixel 拉丁与 CJK 之间的理想间距
ekp-mws-stretch-pixel 拉丁与 CJK 之间的最大拉伸量
ekp-mws-shrink-pixel 拉丁与 CJK 之间的最大压缩量
ekp-cws-ideal-pixel CJK 字符间的理想间距
ekp-cws-stretch-pixel CJK 字符间的最大拉伸量
ekp-cws-shrink-pixel CJK 字符间的最大压缩量

示例:(ekp-param-set 7 3 2 5 2 1 0 2 0)

请勿直接设置这些变量——必须使用 ekp-param-set 函数。

默认值遵循 K-P 推荐:

  • 理想宽度 = 空格字符宽度
  • 可拉伸 = 理想 × 0.5
  • 可压缩 = 理想 × 0.33

K-P 算法参数

参数 默认值 说明
ekp-line-penalty 10 每行断行的基础惩罚
ekp-hyphen-penalty 50 连字符断词的惩罚
ekp-adjacent-fitness-penalty 100 相邻行松紧度不一致的惩罚
ekp-last-line-min-ratio 0.5 末行最小填充比例
ekp-looseness 0 目标行数偏移±n 行)

核心函数

(ekp-pixel-justify string line-pixel)

将 STRING 按 LINE-PIXEL 宽度排版,返回排版后的文本。

(ekp-pixel-range-justify string min-pixel max-pixel)

在 [MIN-PIXEL, MAX-PIXEL] 范围内使用三分搜索寻找最优宽度。 返回 (排版文本 . 最优像素值)

注:使用 O(log n) 三分搜索,并积极缓存。

(ekp-clear-caches)

清除所有段落缓存。

路线图

  • 排版后保留原始文本属性
  • 完整的 Knuth-Plass demerits 模型与 fitness classes
  • 支持连续连字符惩罚的断词
  • Rust 动态模块实现并行计算
  • 混合标点自动修正

致谢