ekp/readme_zh.md
2026-01-24 22:40:53 +08:00

171 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Emacs-KP: Knuth-Plass 排版算法 Emacs 实现
Emacs-kp 实现了 Knuth-Plass 最优断行算法,并扩展支持 CJK中日韩与拉丁文混合排版。
## 演示
![ekp-demo](./images/ekp-demo-with-cache.gif)
## 算法原理
### Knuth-Plass 算法
本算法基于 Donald Knuth 和 Michael Plass 于 1981 年发表的经典论文 ["Breaking Paragraphs into Lines"](http://www.eprg.org/G53DOC/pdfs/knuth-plass-breaking.pdf)。与大多数文本编辑器使用的贪心断行不同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 行) |
### 核心函数
```elisp
(ekp-pixel-justify string line-pixel)
```
将 STRING 按 LINE-PIXEL 宽度排版,返回排版后的文本。
```elisp
(ekp-pixel-range-justify string min-pixel max-pixel)
```
在 [MIN-PIXEL, MAX-PIXEL] 范围内使用三分搜索寻找最优宽度。
返回 `(排版文本 . 最优像素值)`
注:使用 O(log n) 三分搜索,并积极缓存。
```elisp
(ekp-clear-caches)
```
清除所有段落缓存。
## 路线图
- [x] 排版后保留原始文本属性
- [x] 完整的 Knuth-Plass demerits 模型与 fitness classes
- [x] 支持连续连字符惩罚的断词
- [ ] Rust 动态模块实现并行计算
- [ ] 混合标点自动修正
## 致谢
- 核心算法Donald E. Knuth 和 Michael F. Plass 的论文 ["Breaking Paragraphs into Lines"](http://www.eprg.org/G53DOC/pdfs/knuth-plass-breaking.pdf)1981
- 断词算法:改编自 [Pyphen](https://github.com/Kozea/Pyphen),使用 Liang 算法
- 词典:[Hunspell 断词模式](https://github.com/Kozea/Pyphen)