ziban 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ziban-0.1.0/.github/workflows/ci.yml +41 -0
- ziban-0.1.0/.gitignore +11 -0
- ziban-0.1.0/LICENSE +21 -0
- ziban-0.1.0/PKG-INFO +203 -0
- ziban-0.1.0/README.md +180 -0
- ziban-0.1.0/README.zh-CN.md +164 -0
- ziban-0.1.0/docs/compare.png +0 -0
- ziban-0.1.0/examples/compare.py +202 -0
- ziban-0.1.0/pyproject.toml +50 -0
- ziban-0.1.0/src/ziban/__init__.py +56 -0
- ziban-0.1.0/src/ziban/__main__.py +6 -0
- ziban-0.1.0/src/ziban/classify.py +314 -0
- ziban-0.1.0/src/ziban/cli.py +207 -0
- ziban-0.1.0/src/ziban/data/clreq.toml +33 -0
- ziban-0.1.0/src/ziban/layout.py +486 -0
- ziban-0.1.0/src/ziban/metrics.py +121 -0
- ziban-0.1.0/src/ziban/py.typed +0 -0
- ziban-0.1.0/src/ziban/rules.py +126 -0
- ziban-0.1.0/tests/conftest.py +10 -0
- ziban-0.1.0/tests/corpus.py +35 -0
- ziban-0.1.0/tests/test_classify.py +89 -0
- ziban-0.1.0/tests/test_cli.py +73 -0
- ziban-0.1.0/tests/test_invariants.py +89 -0
- ziban-0.1.0/tests/test_layout.py +206 -0
- ziban-0.1.0/tests/test_metrics.py +55 -0
- ziban-0.1.0/tests/test_rules.py +86 -0
- ziban-0.1.0/tests/test_stress.py +54 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: ${{ matrix.os }} / py${{ matrix.python }}
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
16
|
+
python: ["3.9", "3.12", "3.13"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python }}
|
|
22
|
+
- name: Install
|
|
23
|
+
run: python -m pip install -e ".[dev]"
|
|
24
|
+
- name: Test
|
|
25
|
+
run: python -m pytest -q
|
|
26
|
+
|
|
27
|
+
demo:
|
|
28
|
+
name: CLI demo
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
- uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: "3.12"
|
|
35
|
+
- name: Install
|
|
36
|
+
run: python -m pip install -e .
|
|
37
|
+
- name: Break a sentence
|
|
38
|
+
run: |
|
|
39
|
+
printf '他说:「这次不一样。」然后关掉了对话框。' | ziban wrap - --width-em 10 --ruler
|
|
40
|
+
- name: Show the ruleset
|
|
41
|
+
run: ziban rules
|
ziban-0.1.0/.gitignore
ADDED
ziban-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ziban contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ziban-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ziban
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CJK typesetting for programmatic renderers: line breaking, kinsoku, punctuation compression, hanging punctuation.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: chinese,cjk,kinsoku,line-breaking,pdf,pillow,text-layout,typography
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
13
|
+
Classifier: Topic :: Text Processing :: Fonts
|
|
14
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Requires-Dist: tomli>=1.1.0; python_version < '3.11'
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pillow>=9.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
20
|
+
Provides-Extra: pillow
|
|
21
|
+
Requires-Dist: pillow>=9.0; extra == 'pillow'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# ziban
|
|
25
|
+
|
|
26
|
+
**Correct CJK line breaking for everything that is not a browser.**
|
|
27
|
+
|
|
28
|
+
[中文说明 →](README.zh-CN.md)
|
|
29
|
+
|
|
30
|
+
Browsers got `text-spacing-trim`, `line-break: strict` and proper kinsoku years
|
|
31
|
+
ago. Everything else — Pillow, ReportLab, SVG, ffmpeg subtitles, `python-pptx`,
|
|
32
|
+
canvas — still reimplements it badly, because there is nothing to ask.
|
|
33
|
+
|
|
34
|
+
`ziban` is that thing: a pure-Python typesetting engine that turns a string and
|
|
35
|
+
a box into lines a Chinese typesetter would accept — and tells you, in machine
|
|
36
|
+
readable form, every time it had to compromise.
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
## The problem, in eight seconds
|
|
41
|
+
|
|
42
|
+
Naive `if len(s) > N: cut()`:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
他说:「这次不一样。
|
|
46
|
+
」然后关掉了对话框。
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`ziban`:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
他说:「这次不一样。」
|
|
53
|
+
然后关掉了对话框。
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The first version starts a line with `」`. That is not a matter of taste:
|
|
57
|
+
[《中文排版需求》(CLREQ)](https://www.w3.org/TR/clreq/) forbids closing
|
|
58
|
+
punctuation at line start (避头) and opening punctuation at line end (避尾).
|
|
59
|
+
Every renderer that is not a browser quietly breaks that rule — and the
|
|
60
|
+
punctuation it breaks is exactly the punctuation readers notice.
|
|
61
|
+
|
|
62
|
+
The image above is generated, not drawn by hand:
|
|
63
|
+
`python examples/compare.py --font <path-to-a-CJK-font>`. Re-run it after any
|
|
64
|
+
change to the rules and you will see immediately whether the typography got
|
|
65
|
+
better or worse.
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install ziban # core, zero dependencies
|
|
71
|
+
pip install "ziban[pillow]" # + measuring with real font files
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Python 3.9+. No browser, no native build, no ICU, no model.
|
|
75
|
+
|
|
76
|
+
## Use it
|
|
77
|
+
|
|
78
|
+
### As a CLI
|
|
79
|
+
|
|
80
|
+
```console
|
|
81
|
+
$ echo "这是一个比较长的段落,用来看断行在真实文本上的表现。" \
|
|
82
|
+
| ziban wrap - --width-em 8 --ruler
|
|
83
|
+
+----------------+
|
|
84
|
+
1 | 这是一个比较长的 | 8.0em
|
|
85
|
+
2 | 段落,用来看断行 | 8.0em
|
|
86
|
+
3 | 在真实文本上的表 | 8.0em
|
|
87
|
+
4 | 现。 | 2.0em
|
|
88
|
+
|
|
89
|
+
4 line(s), box 8.0em, no diagnostics
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`> ` in the gutter means the line ends with a hanging mark, `<` means a
|
|
93
|
+
compressed one, `!` means it overflowed. `--json` gives you the same thing
|
|
94
|
+
as structured data, for scripts and agents.
|
|
95
|
+
|
|
96
|
+
### As a library
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from ziban import Style, layout, EmMetrics
|
|
100
|
+
|
|
101
|
+
page = layout(
|
|
102
|
+
"他说:「这次不一样。」然后关掉了对话框。",
|
|
103
|
+
width=10 * 32, # 10 characters wide
|
|
104
|
+
style=Style(size=32, leading=1.7),
|
|
105
|
+
metrics=EmMetrics(32), # or PillowMetrics("NotoSerifSC.otf", 32)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
for line in page.lines:
|
|
109
|
+
print(line.text, line.width)
|
|
110
|
+
|
|
111
|
+
page.diagnostics
|
|
112
|
+
# [Diagnostic(code='ORPHAN_LINE', line=1, ...)]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Every line carries its own explanation:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
line.adjustments # (Adjustment(kind='hang', index=10, delta=32.0),)
|
|
119
|
+
line.slack # pixels left over
|
|
120
|
+
line.overflow # pixels past the box, 0 if it fits
|
|
121
|
+
line.justify_spacing
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## What it does
|
|
125
|
+
|
|
126
|
+
| | |
|
|
127
|
+
|---|---|
|
|
128
|
+
| **避头尾 (kinsoku)** | closing marks never start a line, opening marks never end one |
|
|
129
|
+
| **标点挤压 (compression)** | a full-width mark is drawn at half width when that is what makes the line fit |
|
|
130
|
+
| **行末悬挂 (hanging)** | a mark may sit outside the margin rather than be pushed down — capped at one em *and* a quarter of the measure |
|
|
131
|
+
| **中西混排** | breaks between CJK and Latin, never inside a Latin word, numbers stay glued to their units |
|
|
132
|
+
| **硬断行** | `\n` is always honoured, blank lines survive |
|
|
133
|
+
| **诊断** | `OVERFLOW`, `NO_BREAK_OPPORTUNITY`, `KINSOKU_FALLBACK`, `ORPHAN_LINE` — with a suggested fix |
|
|
134
|
+
| **确定性** | same text + same font + same size → byte-identical output, on every platform |
|
|
135
|
+
|
|
136
|
+
## What it does not do
|
|
137
|
+
|
|
138
|
+
- No AI. No API keys. No "generate my copy".
|
|
139
|
+
- No browser, no headless Chromium, no layout engine to install.
|
|
140
|
+
- No GUI editor.
|
|
141
|
+
- No auto-publishing to any platform.
|
|
142
|
+
- **No bundled fonts.** Microsoft YaHei and PingFang are not redistributable;
|
|
143
|
+
bring your own, or use Noto Sans SC / Noto Serif SC (OFL).
|
|
144
|
+
|
|
145
|
+
Being explicit about the last one is not pedantry: a library that ships a
|
|
146
|
+
proprietary font is a library companies cannot use.
|
|
147
|
+
|
|
148
|
+
## Rules are data, not `if`-statements
|
|
149
|
+
|
|
150
|
+
The whole policy lives in [`src/ziban/data/clreq.toml`](src/ziban/data/clreq.toml):
|
|
151
|
+
|
|
152
|
+
```toml
|
|
153
|
+
[kinsoku]
|
|
154
|
+
head_forbidden = " ,、。.;:?!)〕〉》」』】”’}…"
|
|
155
|
+
tail_forbidden = "(〔〈《「『【“‘{"
|
|
156
|
+
|
|
157
|
+
[compress]
|
|
158
|
+
compressed_ratio = 0.5
|
|
159
|
+
chars = ",、。.;:?!)〕〉》」』】”’…"
|
|
160
|
+
|
|
161
|
+
[hang]
|
|
162
|
+
max_ratio = 1.0
|
|
163
|
+
max_share = 0.25
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Change the file to change the behaviour. A Japanese ruleset is a second TOML,
|
|
167
|
+
not a second engine. This is also why contributing is cheap: most typographic
|
|
168
|
+
bugs are one line of data.
|
|
169
|
+
|
|
170
|
+
## How it compares
|
|
171
|
+
|
|
172
|
+
| | kinsoku | compression | hanging | works outside a browser | deterministic |
|
|
173
|
+
|---|---|---|---|---|---|
|
|
174
|
+
| [heti](https://github.com/sivan/heti) | via browser | partial | ✗ | ✗ | ✗ |
|
|
175
|
+
| [pangu.py](https://github.com/vinta/pangu.py) | ✗ | ✗ | ✗ | ✓ | ✓ |
|
|
176
|
+
| [budoux](https://github.com/google/budoux) | ✗ | ✗ | ✗ | ✓ | ✓ |
|
|
177
|
+
| browser `text-spacing-trim` | ✓ | ✓ | partial | ✗ | ✓ |
|
|
178
|
+
| **ziban** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
179
|
+
|
|
180
|
+
`budoux` decides *where* a break is semantically acceptable; `ziban` decides
|
|
181
|
+
whether the break is typographically legal. They compose.
|
|
182
|
+
|
|
183
|
+
## Status
|
|
184
|
+
|
|
185
|
+
`0.1.0` — engine complete for horizontal text, 958 tests green
|
|
186
|
+
(144 corpus × width combinations (24 paragraphs × 6 measures) checked against six invariants, plus golden
|
|
187
|
+
cases). Not yet on PyPI.
|
|
188
|
+
|
|
189
|
+
Next:
|
|
190
|
+
|
|
191
|
+
- **0.2** font probing across Windows/macOS/Linux, pair compression (「。」 in
|
|
192
|
+
one em), a Knuth–Plass-style cost function so the last line stops being an
|
|
193
|
+
orphan
|
|
194
|
+
- **0.3** adapters: ReportLab, SVG, `python-pptx`; vertical text behind a flag
|
|
195
|
+
- **0.4** a `cards` example that renders real 1080×1440 image sets, and a
|
|
196
|
+
`SKILL.md` wrapper so agents can call it
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT. See [LICENSE](LICENSE).
|
|
201
|
+
|
|
202
|
+
The ruleset follows [CLREQ](https://www.w3.org/TR/clreq/) (W3C). Character
|
|
203
|
+
classes follow a curated subset of [UAX #14](https://www.unicode.org/reports/tr14/).
|
ziban-0.1.0/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# ziban
|
|
2
|
+
|
|
3
|
+
**Correct CJK line breaking for everything that is not a browser.**
|
|
4
|
+
|
|
5
|
+
[中文说明 →](README.zh-CN.md)
|
|
6
|
+
|
|
7
|
+
Browsers got `text-spacing-trim`, `line-break: strict` and proper kinsoku years
|
|
8
|
+
ago. Everything else — Pillow, ReportLab, SVG, ffmpeg subtitles, `python-pptx`,
|
|
9
|
+
canvas — still reimplements it badly, because there is nothing to ask.
|
|
10
|
+
|
|
11
|
+
`ziban` is that thing: a pure-Python typesetting engine that turns a string and
|
|
12
|
+
a box into lines a Chinese typesetter would accept — and tells you, in machine
|
|
13
|
+
readable form, every time it had to compromise.
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
## The problem, in eight seconds
|
|
18
|
+
|
|
19
|
+
Naive `if len(s) > N: cut()`:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
他说:「这次不一样。
|
|
23
|
+
」然后关掉了对话框。
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`ziban`:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
他说:「这次不一样。」
|
|
30
|
+
然后关掉了对话框。
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The first version starts a line with `」`. That is not a matter of taste:
|
|
34
|
+
[《中文排版需求》(CLREQ)](https://www.w3.org/TR/clreq/) forbids closing
|
|
35
|
+
punctuation at line start (避头) and opening punctuation at line end (避尾).
|
|
36
|
+
Every renderer that is not a browser quietly breaks that rule — and the
|
|
37
|
+
punctuation it breaks is exactly the punctuation readers notice.
|
|
38
|
+
|
|
39
|
+
The image above is generated, not drawn by hand:
|
|
40
|
+
`python examples/compare.py --font <path-to-a-CJK-font>`. Re-run it after any
|
|
41
|
+
change to the rules and you will see immediately whether the typography got
|
|
42
|
+
better or worse.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install ziban # core, zero dependencies
|
|
48
|
+
pip install "ziban[pillow]" # + measuring with real font files
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Python 3.9+. No browser, no native build, no ICU, no model.
|
|
52
|
+
|
|
53
|
+
## Use it
|
|
54
|
+
|
|
55
|
+
### As a CLI
|
|
56
|
+
|
|
57
|
+
```console
|
|
58
|
+
$ echo "这是一个比较长的段落,用来看断行在真实文本上的表现。" \
|
|
59
|
+
| ziban wrap - --width-em 8 --ruler
|
|
60
|
+
+----------------+
|
|
61
|
+
1 | 这是一个比较长的 | 8.0em
|
|
62
|
+
2 | 段落,用来看断行 | 8.0em
|
|
63
|
+
3 | 在真实文本上的表 | 8.0em
|
|
64
|
+
4 | 现。 | 2.0em
|
|
65
|
+
|
|
66
|
+
4 line(s), box 8.0em, no diagnostics
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`> ` in the gutter means the line ends with a hanging mark, `<` means a
|
|
70
|
+
compressed one, `!` means it overflowed. `--json` gives you the same thing
|
|
71
|
+
as structured data, for scripts and agents.
|
|
72
|
+
|
|
73
|
+
### As a library
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from ziban import Style, layout, EmMetrics
|
|
77
|
+
|
|
78
|
+
page = layout(
|
|
79
|
+
"他说:「这次不一样。」然后关掉了对话框。",
|
|
80
|
+
width=10 * 32, # 10 characters wide
|
|
81
|
+
style=Style(size=32, leading=1.7),
|
|
82
|
+
metrics=EmMetrics(32), # or PillowMetrics("NotoSerifSC.otf", 32)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
for line in page.lines:
|
|
86
|
+
print(line.text, line.width)
|
|
87
|
+
|
|
88
|
+
page.diagnostics
|
|
89
|
+
# [Diagnostic(code='ORPHAN_LINE', line=1, ...)]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Every line carries its own explanation:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
line.adjustments # (Adjustment(kind='hang', index=10, delta=32.0),)
|
|
96
|
+
line.slack # pixels left over
|
|
97
|
+
line.overflow # pixels past the box, 0 if it fits
|
|
98
|
+
line.justify_spacing
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## What it does
|
|
102
|
+
|
|
103
|
+
| | |
|
|
104
|
+
|---|---|
|
|
105
|
+
| **避头尾 (kinsoku)** | closing marks never start a line, opening marks never end one |
|
|
106
|
+
| **标点挤压 (compression)** | a full-width mark is drawn at half width when that is what makes the line fit |
|
|
107
|
+
| **行末悬挂 (hanging)** | a mark may sit outside the margin rather than be pushed down — capped at one em *and* a quarter of the measure |
|
|
108
|
+
| **中西混排** | breaks between CJK and Latin, never inside a Latin word, numbers stay glued to their units |
|
|
109
|
+
| **硬断行** | `\n` is always honoured, blank lines survive |
|
|
110
|
+
| **诊断** | `OVERFLOW`, `NO_BREAK_OPPORTUNITY`, `KINSOKU_FALLBACK`, `ORPHAN_LINE` — with a suggested fix |
|
|
111
|
+
| **确定性** | same text + same font + same size → byte-identical output, on every platform |
|
|
112
|
+
|
|
113
|
+
## What it does not do
|
|
114
|
+
|
|
115
|
+
- No AI. No API keys. No "generate my copy".
|
|
116
|
+
- No browser, no headless Chromium, no layout engine to install.
|
|
117
|
+
- No GUI editor.
|
|
118
|
+
- No auto-publishing to any platform.
|
|
119
|
+
- **No bundled fonts.** Microsoft YaHei and PingFang are not redistributable;
|
|
120
|
+
bring your own, or use Noto Sans SC / Noto Serif SC (OFL).
|
|
121
|
+
|
|
122
|
+
Being explicit about the last one is not pedantry: a library that ships a
|
|
123
|
+
proprietary font is a library companies cannot use.
|
|
124
|
+
|
|
125
|
+
## Rules are data, not `if`-statements
|
|
126
|
+
|
|
127
|
+
The whole policy lives in [`src/ziban/data/clreq.toml`](src/ziban/data/clreq.toml):
|
|
128
|
+
|
|
129
|
+
```toml
|
|
130
|
+
[kinsoku]
|
|
131
|
+
head_forbidden = " ,、。.;:?!)〕〉》」』】”’}…"
|
|
132
|
+
tail_forbidden = "(〔〈《「『【“‘{"
|
|
133
|
+
|
|
134
|
+
[compress]
|
|
135
|
+
compressed_ratio = 0.5
|
|
136
|
+
chars = ",、。.;:?!)〕〉》」』】”’…"
|
|
137
|
+
|
|
138
|
+
[hang]
|
|
139
|
+
max_ratio = 1.0
|
|
140
|
+
max_share = 0.25
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Change the file to change the behaviour. A Japanese ruleset is a second TOML,
|
|
144
|
+
not a second engine. This is also why contributing is cheap: most typographic
|
|
145
|
+
bugs are one line of data.
|
|
146
|
+
|
|
147
|
+
## How it compares
|
|
148
|
+
|
|
149
|
+
| | kinsoku | compression | hanging | works outside a browser | deterministic |
|
|
150
|
+
|---|---|---|---|---|---|
|
|
151
|
+
| [heti](https://github.com/sivan/heti) | via browser | partial | ✗ | ✗ | ✗ |
|
|
152
|
+
| [pangu.py](https://github.com/vinta/pangu.py) | ✗ | ✗ | ✗ | ✓ | ✓ |
|
|
153
|
+
| [budoux](https://github.com/google/budoux) | ✗ | ✗ | ✗ | ✓ | ✓ |
|
|
154
|
+
| browser `text-spacing-trim` | ✓ | ✓ | partial | ✗ | ✓ |
|
|
155
|
+
| **ziban** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
156
|
+
|
|
157
|
+
`budoux` decides *where* a break is semantically acceptable; `ziban` decides
|
|
158
|
+
whether the break is typographically legal. They compose.
|
|
159
|
+
|
|
160
|
+
## Status
|
|
161
|
+
|
|
162
|
+
`0.1.0` — engine complete for horizontal text, 958 tests green
|
|
163
|
+
(144 corpus × width combinations (24 paragraphs × 6 measures) checked against six invariants, plus golden
|
|
164
|
+
cases). Not yet on PyPI.
|
|
165
|
+
|
|
166
|
+
Next:
|
|
167
|
+
|
|
168
|
+
- **0.2** font probing across Windows/macOS/Linux, pair compression (「。」 in
|
|
169
|
+
one em), a Knuth–Plass-style cost function so the last line stops being an
|
|
170
|
+
orphan
|
|
171
|
+
- **0.3** adapters: ReportLab, SVG, `python-pptx`; vertical text behind a flag
|
|
172
|
+
- **0.4** a `cards` example that renders real 1080×1440 image sets, and a
|
|
173
|
+
`SKILL.md` wrapper so agents can call it
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT. See [LICENSE](LICENSE).
|
|
178
|
+
|
|
179
|
+
The ruleset follows [CLREQ](https://www.w3.org/TR/clreq/) (W3C). Character
|
|
180
|
+
classes follow a curated subset of [UAX #14](https://www.unicode.org/reports/tr14/).
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# ziban
|
|
2
|
+
|
|
3
|
+
**给"浏览器之外"的一切,做对中文断行。**
|
|
4
|
+
|
|
5
|
+
[English →](README.md)
|
|
6
|
+
|
|
7
|
+
浏览器很多年前就有了 `text-spacing-trim`、`line-break: strict` 和真正的禁则处理。
|
|
8
|
+
但除了浏览器,别的地方全都得自己重写一遍——Pillow、ReportLab、SVG、ffmpeg 烧字幕、
|
|
9
|
+
`python-pptx`、canvas——而且几乎都写错了,因为**没有一个地方可以去问**。
|
|
10
|
+
|
|
11
|
+
`ziban` 就是那个地方:一个纯 Python 的中文排版引擎,把「一段文字 + 一个盒子」
|
|
12
|
+
变成排版师认可的若干行,并且在每次不得不妥协时,给出机器可读的说明。
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
## 八秒钟看懂问题
|
|
17
|
+
|
|
18
|
+
`if len(s) > N: cut()` 的结果:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
他说:「这次不一样。
|
|
22
|
+
」然后关掉了对话框。
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`ziban`:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
他说:「这次不一样。」
|
|
29
|
+
然后关掉了对话框。
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
第一行以 `」` 开头。这不是审美问题:[《中文排版需求》(CLREQ)](https://www.w3.org/TR/clreq/)
|
|
33
|
+
明确规定收尾标点不可出现在行首(避头)、起首标点不可出现在行尾(避尾)。
|
|
34
|
+
所有非浏览器的渲染器都在默默违反这条规则——而它违反的,恰恰是读者一眼就能看见的那些标点。
|
|
35
|
+
|
|
36
|
+
## 安装
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install ziban # 核心,零依赖
|
|
40
|
+
pip install "ziban[pillow]" # 需要用真实字体度量时
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Python 3.9+。不要浏览器、不要编译、不要 ICU、不要模型。
|
|
44
|
+
|
|
45
|
+
## 用法
|
|
46
|
+
|
|
47
|
+
### 命令行
|
|
48
|
+
|
|
49
|
+
```console
|
|
50
|
+
$ echo "这是一个比较长的段落,用来看断行在真实文本上的表现。" \
|
|
51
|
+
| ziban wrap - --width-em 8 --ruler
|
|
52
|
+
+----------------+
|
|
53
|
+
1 | 这是一个比较长的 | 8.0em
|
|
54
|
+
2 | 段落,用来看断行 | 8.0em
|
|
55
|
+
3 | 在真实文本上的表 | 8.0em
|
|
56
|
+
4 | 现。 | 2.0em
|
|
57
|
+
|
|
58
|
+
4 line(s), box 8.0em, no diagnostics
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
行号后面那一列:`>` 表示本行末尾有标点悬挂,`<` 表示有标点挤压,`!` 表示溢出。
|
|
62
|
+
加 `--json` 得到结构化结果,给脚本和 agent 用。
|
|
63
|
+
|
|
64
|
+
### 库
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from ziban import Style, layout, EmMetrics
|
|
68
|
+
|
|
69
|
+
page = layout(
|
|
70
|
+
"他说:「这次不一样。」然后关掉了对话框。",
|
|
71
|
+
width=10 * 32, # 一行 10 个字
|
|
72
|
+
style=Style(size=32, leading=1.7),
|
|
73
|
+
metrics=EmMetrics(32), # 或 PillowMetrics("NotoSerifSC.otf", 32)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
for line in page.lines:
|
|
77
|
+
print(line.text, line.width)
|
|
78
|
+
|
|
79
|
+
page.diagnostics
|
|
80
|
+
# [Diagnostic(code='ORPHAN_LINE', line=1, ...)]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
每一行都带着自己的解释:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
line.adjustments # (Adjustment(kind='hang', index=10, delta=32.0),)
|
|
87
|
+
line.slack # 剩余空间
|
|
88
|
+
line.overflow # 超出盒子的像素,放得下就是 0
|
|
89
|
+
line.justify_spacing
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 它做了什么
|
|
93
|
+
|
|
94
|
+
| | |
|
|
95
|
+
|---|---|
|
|
96
|
+
| **避头尾** | 收尾标点不进行首,起首标点不留行尾 |
|
|
97
|
+
| **标点挤压** | 差一点点放得下时,全角标点压到半宽 |
|
|
98
|
+
| **行末悬挂** | 宁可让标点悬出边界,也不把它挤到下一行;上限 1 em,且不超过行宽的 1/4 |
|
|
99
|
+
| **中西混排** | 汉字与拉丁文之间可断,拉丁单词内部不断,数字不与单位分家 |
|
|
100
|
+
| **硬断行** | `\n` 一定生效,空行保留 |
|
|
101
|
+
| **诊断** | `OVERFLOW`、`NO_BREAK_OPPORTUNITY`、`KINSOKU_FALLBACK`、`ORPHAN_LINE`,每条都带修复建议 |
|
|
102
|
+
| **确定性** | 同文本 + 同字体 + 同字号 → 逐字节相同的输出,跨平台一致 |
|
|
103
|
+
|
|
104
|
+
## 它不做什么
|
|
105
|
+
|
|
106
|
+
- 不调 AI,不要 API key,不做"帮我写文案"。
|
|
107
|
+
- 不要浏览器、不要无头 Chromium、不要装排版引擎。
|
|
108
|
+
- 不做 GUI 编辑器。
|
|
109
|
+
- 不自动发布到任何平台。
|
|
110
|
+
- **不打包任何字体。** 微软雅黑、苹方都不能随包分发;要么自带,要么用思源黑体/Noto(OFL)。
|
|
111
|
+
|
|
112
|
+
最后一条不是洁癖:一个内置专有字体的库,公司是不能用的。
|
|
113
|
+
|
|
114
|
+
## 规则是数据,不是 if-else
|
|
115
|
+
|
|
116
|
+
所有排版策略都在 [`src/ziban/data/clreq.toml`](src/ziban/data/clreq.toml) 里:
|
|
117
|
+
|
|
118
|
+
```toml
|
|
119
|
+
[kinsoku]
|
|
120
|
+
head_forbidden = " ,、。.;:?!)〕〉》」』】”’}…"
|
|
121
|
+
tail_forbidden = "(〔〈《「『【“‘{"
|
|
122
|
+
|
|
123
|
+
[compress]
|
|
124
|
+
compressed_ratio = 0.5
|
|
125
|
+
chars = ",、。.;:?!)〕〉》」』】”’…"
|
|
126
|
+
|
|
127
|
+
[hang]
|
|
128
|
+
max_ratio = 1.0
|
|
129
|
+
max_share = 0.25
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
改行为就改这张表。日文规则集是第二个 TOML,不是第二个引擎。这也是为什么贡献很便宜:
|
|
133
|
+
**绝大多数排版 bug 只值一行数据。**
|
|
134
|
+
|
|
135
|
+
## 和它们比
|
|
136
|
+
|
|
137
|
+
| | 禁则 | 挤压 | 悬挂 | 浏览器之外可用 | 确定性 |
|
|
138
|
+
|---|---|---|---|---|---|
|
|
139
|
+
| [heti](https://github.com/sivan/heti) | 靠浏览器 | 部分 | ✗ | ✗ | ✗ |
|
|
140
|
+
| [pangu.py](https://github.com/vinta/pangu.py) | ✗ | ✗ | ✗ | ✓ | ✓ |
|
|
141
|
+
| [budoux](https://github.com/google/budoux) | ✗ | ✗ | ✗ | ✓ | ✓ |
|
|
142
|
+
| 浏览器 `text-spacing-trim` | ✓ | ✓ | 部分 | ✗ | ✓ |
|
|
143
|
+
| **ziban** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
144
|
+
|
|
145
|
+
`budoux` 决定"断在哪里语义上说得过去",`ziban` 决定"这个断点排版上合不合法"。两者可以叠加。
|
|
146
|
+
|
|
147
|
+
## 状态
|
|
148
|
+
|
|
149
|
+
`0.1.0` —— 横排引擎完成,958 个测试全绿(24 段语料 × 6 种行宽 × 6 条不变量,
|
|
150
|
+
外加金标用例)。尚未发布到 PyPI。
|
|
151
|
+
|
|
152
|
+
接下来:
|
|
153
|
+
|
|
154
|
+
- **0.2** 跨平台字体探测;连续标点挤压(「。」占一个 em);Knuth–Plass 式代价断行,
|
|
155
|
+
让最后一行不再孤零零
|
|
156
|
+
- **0.3** 适配器:ReportLab、SVG、`python-pptx`;竖排放到开关后面
|
|
157
|
+
- **0.4** `cards` 示例:真正渲染 1080×1440 组图;再包一层 `SKILL.md` 让 agent 能调用
|
|
158
|
+
|
|
159
|
+
## 许可
|
|
160
|
+
|
|
161
|
+
MIT,见 [LICENSE](LICENSE)。
|
|
162
|
+
|
|
163
|
+
规则集依照 [CLREQ](https://www.w3.org/TR/clreq/)(W3C);字符分类是
|
|
164
|
+
[UAX #14](https://www.unicode.org/reports/tr14/) 的裁剪子集。
|
|
Binary file
|