ufo-spacing-lib 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.
- ufo_spacing_lib-0.1.0/.gitignore +49 -0
- ufo_spacing_lib-0.1.0/LICENSE +22 -0
- ufo_spacing_lib-0.1.0/PKG-INFO +299 -0
- ufo_spacing_lib-0.1.0/README.md +268 -0
- ufo_spacing_lib-0.1.0/pyproject.toml +81 -0
- ufo_spacing_lib-0.1.0/src/ufo_spacing_lib/__init__.py +123 -0
- ufo_spacing_lib-0.1.0/src/ufo_spacing_lib/commands/__init__.py +55 -0
- ufo_spacing_lib-0.1.0/src/ufo_spacing_lib/commands/base.py +213 -0
- ufo_spacing_lib-0.1.0/src/ufo_spacing_lib/commands/kerning.py +407 -0
- ufo_spacing_lib-0.1.0/src/ufo_spacing_lib/commands/margins.py +541 -0
- ufo_spacing_lib-0.1.0/src/ufo_spacing_lib/contexts.py +243 -0
- ufo_spacing_lib-0.1.0/src/ufo_spacing_lib/editors/__init__.py +42 -0
- ufo_spacing_lib-0.1.0/src/ufo_spacing_lib/editors/kerning.py +332 -0
- ufo_spacing_lib-0.1.0/src/ufo_spacing_lib/editors/margins.py +294 -0
- ufo_spacing_lib-0.1.0/src/ufo_spacing_lib/groups_core.py +1389 -0
- ufo_spacing_lib-0.1.0/src/ufo_spacing_lib/virtual.py +332 -0
- ufo_spacing_lib-0.1.0/tests/__init__.py +13 -0
- ufo_spacing_lib-0.1.0/tests/mocks.py +372 -0
- ufo_spacing_lib-0.1.0/tests/test_editors.py +301 -0
- ufo_spacing_lib-0.1.0/tests/test_groups_manager.py +409 -0
- ufo_spacing_lib-0.1.0/tests/test_kerning_commands.py +306 -0
- ufo_spacing_lib-0.1.0/tests/test_virtual.py +389 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
|
|
28
|
+
# IDE
|
|
29
|
+
.idea/
|
|
30
|
+
.vscode/
|
|
31
|
+
*.swp
|
|
32
|
+
*.swo
|
|
33
|
+
.cursor/
|
|
34
|
+
|
|
35
|
+
# Testing
|
|
36
|
+
.pytest_cache/
|
|
37
|
+
.coverage
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
|
|
42
|
+
# Type checking
|
|
43
|
+
.mypy_cache/
|
|
44
|
+
|
|
45
|
+
# Build
|
|
46
|
+
dist/
|
|
47
|
+
*.whl
|
|
48
|
+
PUBLISHING.md
|
|
49
|
+
.claude/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alexander Lubovenko
|
|
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.
|
|
22
|
+
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ufo-spacing-lib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Font spacing management library for UFO-compatible fonts (kerning, margins, groups)
|
|
5
|
+
Project-URL: Homepage, https://github.com/typedev/ufo-spacing-lib
|
|
6
|
+
Project-URL: Repository, https://github.com/typedev/ufo-spacing-lib
|
|
7
|
+
Project-URL: Documentation, https://github.com/typedev/ufo-spacing-lib#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/typedev/ufo-spacing-lib/issues
|
|
9
|
+
Author-email: Alexander Lubovenko <lubovenko@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: font,fonttools,kerning,spacing,type-design,typography,ufo
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
22
|
+
Classifier: Topic :: Text Processing :: Fonts
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# UFO Spacing Library
|
|
33
|
+
|
|
34
|
+
A framework-agnostic Python library for managing font spacing (kerning and margins) with full undo/redo support. Designed to work with UFO-compatible font objects.
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- **Framework Independent**: Works with any font editor that provides compatible font objects
|
|
39
|
+
- **Undo/Redo Support**: Full command pattern implementation with unlimited history
|
|
40
|
+
- **Multi-Font Operations**: Support for linked/interpolated fonts with per-font scaling
|
|
41
|
+
- **Preview/Simulation**: VirtualFont wrapper for testing changes without modifying real font
|
|
42
|
+
- **Composite Propagation**: Automatic margin propagation to composite glyphs
|
|
43
|
+
- **Well Documented**: Comprehensive docstrings and type hints throughout
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# From PyPI (when published)
|
|
49
|
+
pip install ufo-spacing-lib
|
|
50
|
+
|
|
51
|
+
# From source
|
|
52
|
+
pip install -e .
|
|
53
|
+
|
|
54
|
+
# Or with uv
|
|
55
|
+
uv pip install -e .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
### Kerning Operations
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from ufo_spacing_lib import (
|
|
64
|
+
KerningEditor,
|
|
65
|
+
FontContext,
|
|
66
|
+
AdjustKerningCommand,
|
|
67
|
+
SetKerningCommand,
|
|
68
|
+
RemoveKerningCommand,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Create an editor
|
|
72
|
+
editor = KerningEditor()
|
|
73
|
+
|
|
74
|
+
# Create a context for your font
|
|
75
|
+
context = FontContext.from_single_font(font)
|
|
76
|
+
|
|
77
|
+
# Adjust kerning by a delta
|
|
78
|
+
cmd = AdjustKerningCommand(pair=('A', 'V'), delta=-10)
|
|
79
|
+
result = editor.execute(cmd, context)
|
|
80
|
+
|
|
81
|
+
# Set kerning to absolute value
|
|
82
|
+
cmd = SetKerningCommand(pair=('A', 'V'), value=-50)
|
|
83
|
+
editor.execute(cmd, context)
|
|
84
|
+
|
|
85
|
+
# Remove a kerning pair
|
|
86
|
+
cmd = RemoveKerningCommand(pair=('A', 'V'))
|
|
87
|
+
editor.execute(cmd, context)
|
|
88
|
+
|
|
89
|
+
# Undo/Redo
|
|
90
|
+
editor.undo()
|
|
91
|
+
editor.redo()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Margins Operations
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from ufo_spacing_lib import (
|
|
98
|
+
MarginsEditor,
|
|
99
|
+
FontContext,
|
|
100
|
+
AdjustMarginCommand,
|
|
101
|
+
SetMarginCommand,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
editor = MarginsEditor()
|
|
105
|
+
context = FontContext.from_single_font(font)
|
|
106
|
+
|
|
107
|
+
# Adjust left margin (propagates to composites by default)
|
|
108
|
+
cmd = AdjustMarginCommand(
|
|
109
|
+
glyph_name='A',
|
|
110
|
+
side='left',
|
|
111
|
+
delta=10,
|
|
112
|
+
propagate_to_composites=True
|
|
113
|
+
)
|
|
114
|
+
editor.execute(cmd, context)
|
|
115
|
+
|
|
116
|
+
# Set right margin to absolute value
|
|
117
|
+
cmd = SetMarginCommand(
|
|
118
|
+
glyph_name='A',
|
|
119
|
+
side='right',
|
|
120
|
+
value=50
|
|
121
|
+
)
|
|
122
|
+
editor.execute(cmd, context)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Multi-Font Operations (Interpolation)
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# Create context for multiple fonts with scaling
|
|
129
|
+
context = FontContext.from_linked_fonts(
|
|
130
|
+
fonts=[light_master, regular_master, bold_master],
|
|
131
|
+
primary=regular_master,
|
|
132
|
+
scales={
|
|
133
|
+
light_master: 0.8,
|
|
134
|
+
regular_master: 1.0,
|
|
135
|
+
bold_master: 1.3
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Command applies to all fonts with appropriate scaling
|
|
140
|
+
cmd = AdjustKerningCommand(pair=('A', 'V'), delta=-10)
|
|
141
|
+
editor.execute(cmd, context)
|
|
142
|
+
# light_master: -8, regular_master: -10, bold_master: -13
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Event Callbacks
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
def on_kerning_change(command, result):
|
|
149
|
+
print(f"Kerning changed: {command.description}")
|
|
150
|
+
refresh_ui()
|
|
151
|
+
|
|
152
|
+
editor.on_change = on_kerning_change
|
|
153
|
+
editor.on_undo = on_kerning_change
|
|
154
|
+
editor.on_redo = on_kerning_change
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Preview/Simulation (VirtualFont)
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from ufo_spacing_lib import VirtualFont, FontContext, AdjustKerningCommand
|
|
161
|
+
|
|
162
|
+
# Create virtual copy - isolates kerning/groups changes
|
|
163
|
+
virtual = VirtualFont.from_font(font)
|
|
164
|
+
|
|
165
|
+
# Work as usual - changes only affect virtual.kerning/groups
|
|
166
|
+
context = FontContext.from_single_font(virtual)
|
|
167
|
+
cmd = AdjustKerningCommand(pair=('A', 'V'), delta=-10)
|
|
168
|
+
editor.execute(cmd, context)
|
|
169
|
+
|
|
170
|
+
# Glyphs are live references - changes in font visible through virtual
|
|
171
|
+
print(virtual['A'].leftMargin) # Same as font['A'].leftMargin
|
|
172
|
+
|
|
173
|
+
# Check what changed
|
|
174
|
+
if virtual.has_changes():
|
|
175
|
+
for pair, (old, new) in virtual.get_kerning_diff().items():
|
|
176
|
+
print(f"{pair}: {old} -> {new}")
|
|
177
|
+
|
|
178
|
+
# Apply to real font when ready, or reset
|
|
179
|
+
virtual.apply_to(font) # Writes changes to font
|
|
180
|
+
# virtual.reset() # Discards all changes
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Architecture
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
ufo_spacing_lib/
|
|
187
|
+
├── __init__.py # Main exports
|
|
188
|
+
├── contexts.py # FontContext class
|
|
189
|
+
├── groups_core.py # FontGroupsManager, KernPairInfo, resolve_kern_pair
|
|
190
|
+
├── virtual.py # VirtualFont for preview/simulation
|
|
191
|
+
├── commands/
|
|
192
|
+
│ ├── __init__.py
|
|
193
|
+
│ ├── base.py # Command ABC, CommandResult
|
|
194
|
+
│ ├── kerning.py # Kerning commands
|
|
195
|
+
│ └── margins.py # Margins commands
|
|
196
|
+
└── editors/
|
|
197
|
+
├── __init__.py
|
|
198
|
+
├── kerning.py # KerningEditor
|
|
199
|
+
└── margins.py # MarginsEditor
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Font Object Interface
|
|
203
|
+
|
|
204
|
+
The library is designed to work with any font object that implements this interface:
|
|
205
|
+
|
|
206
|
+
### For Kerning Operations
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
class FontKerning:
|
|
210
|
+
"""Dict-like kerning access."""
|
|
211
|
+
def __getitem__(self, pair: Tuple[str, str]) -> int: ...
|
|
212
|
+
def __setitem__(self, pair: Tuple[str, str], value: int): ...
|
|
213
|
+
def __delitem__(self, pair: Tuple[str, str]): ...
|
|
214
|
+
def __contains__(self, pair: Tuple[str, str]) -> bool: ...
|
|
215
|
+
def get(self, pair: Tuple[str, str], default=None) -> Optional[int]: ...
|
|
216
|
+
|
|
217
|
+
class Font:
|
|
218
|
+
kerning: FontKerning
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### For Margins Operations
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
class Glyph:
|
|
225
|
+
leftMargin: Optional[int]
|
|
226
|
+
rightMargin: Optional[int]
|
|
227
|
+
width: int
|
|
228
|
+
components: List[Component] # Optional
|
|
229
|
+
|
|
230
|
+
def moveBy(self, delta: Tuple[int, int]): ...
|
|
231
|
+
def changed(self): ... # Optional
|
|
232
|
+
|
|
233
|
+
class Component:
|
|
234
|
+
offset: Tuple[int, int]
|
|
235
|
+
def moveBy(self, delta: Tuple[int, int]): ...
|
|
236
|
+
|
|
237
|
+
class Font:
|
|
238
|
+
def __getitem__(self, glyph_name: str) -> Glyph: ...
|
|
239
|
+
def __contains__(self, glyph_name: str) -> bool: ...
|
|
240
|
+
def getReverseComponentMapping(self) -> Dict[str, List[str]]: ... # Optional
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Commands Reference
|
|
244
|
+
|
|
245
|
+
### Kerning Commands
|
|
246
|
+
|
|
247
|
+
| Command | Description |
|
|
248
|
+
|---------|-------------|
|
|
249
|
+
| `SetKerningCommand(pair, value)` | Set kerning to absolute value |
|
|
250
|
+
| `AdjustKerningCommand(pair, delta)` | Adjust kerning by delta |
|
|
251
|
+
| `RemoveKerningCommand(pair)` | Remove a kerning pair |
|
|
252
|
+
| `CreateExceptionCommand(pair, value, side)` | Create kerning exception |
|
|
253
|
+
|
|
254
|
+
### Margins Commands
|
|
255
|
+
|
|
256
|
+
| Command | Description |
|
|
257
|
+
|---------|-------------|
|
|
258
|
+
| `SetMarginCommand(glyph, side, value)` | Set margin to absolute value |
|
|
259
|
+
| `AdjustMarginCommand(glyph, side, delta)` | Adjust margin by delta |
|
|
260
|
+
|
|
261
|
+
All commands support:
|
|
262
|
+
- Multi-font operations via `FontContext`
|
|
263
|
+
- Per-font scaling
|
|
264
|
+
- Full undo/redo
|
|
265
|
+
|
|
266
|
+
## Testing
|
|
267
|
+
|
|
268
|
+
The library includes 100+ unit tests covering all components.
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
# Run all tests
|
|
272
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
273
|
+
|
|
274
|
+
# Run specific test module
|
|
275
|
+
PYTHONPATH=src python3 -m unittest tests.test_kerning_commands -v
|
|
276
|
+
|
|
277
|
+
# With pytest (if installed)
|
|
278
|
+
PYTHONPATH=src python3 -m pytest tests/ -v
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Test Coverage
|
|
282
|
+
|
|
283
|
+
| Module | Tests | Coverage |
|
|
284
|
+
|--------|-------|----------|
|
|
285
|
+
| Kerning Commands | 25 | SetKerning, AdjustKerning, RemoveKerning, CreateException |
|
|
286
|
+
| Editors | 20 | KerningEditor, MarginsEditor, undo/redo, callbacks |
|
|
287
|
+
| Groups Manager | 30 | FontGroupsManager, add/remove/delete/rename groups |
|
|
288
|
+
| VirtualFont | 27 | Creation, isolation, glyph access, diff tracking, apply/reset |
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
MIT License
|
|
293
|
+
|
|
294
|
+
## Author
|
|
295
|
+
|
|
296
|
+
Alexander Lubovenko
|
|
297
|
+
lubovenko@gmail.com
|
|
298
|
+
github.com/typedev
|
|
299
|
+
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# UFO Spacing Library
|
|
2
|
+
|
|
3
|
+
A framework-agnostic Python library for managing font spacing (kerning and margins) with full undo/redo support. Designed to work with UFO-compatible font objects.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Framework Independent**: Works with any font editor that provides compatible font objects
|
|
8
|
+
- **Undo/Redo Support**: Full command pattern implementation with unlimited history
|
|
9
|
+
- **Multi-Font Operations**: Support for linked/interpolated fonts with per-font scaling
|
|
10
|
+
- **Preview/Simulation**: VirtualFont wrapper for testing changes without modifying real font
|
|
11
|
+
- **Composite Propagation**: Automatic margin propagation to composite glyphs
|
|
12
|
+
- **Well Documented**: Comprehensive docstrings and type hints throughout
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# From PyPI (when published)
|
|
18
|
+
pip install ufo-spacing-lib
|
|
19
|
+
|
|
20
|
+
# From source
|
|
21
|
+
pip install -e .
|
|
22
|
+
|
|
23
|
+
# Or with uv
|
|
24
|
+
uv pip install -e .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### Kerning Operations
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from ufo_spacing_lib import (
|
|
33
|
+
KerningEditor,
|
|
34
|
+
FontContext,
|
|
35
|
+
AdjustKerningCommand,
|
|
36
|
+
SetKerningCommand,
|
|
37
|
+
RemoveKerningCommand,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Create an editor
|
|
41
|
+
editor = KerningEditor()
|
|
42
|
+
|
|
43
|
+
# Create a context for your font
|
|
44
|
+
context = FontContext.from_single_font(font)
|
|
45
|
+
|
|
46
|
+
# Adjust kerning by a delta
|
|
47
|
+
cmd = AdjustKerningCommand(pair=('A', 'V'), delta=-10)
|
|
48
|
+
result = editor.execute(cmd, context)
|
|
49
|
+
|
|
50
|
+
# Set kerning to absolute value
|
|
51
|
+
cmd = SetKerningCommand(pair=('A', 'V'), value=-50)
|
|
52
|
+
editor.execute(cmd, context)
|
|
53
|
+
|
|
54
|
+
# Remove a kerning pair
|
|
55
|
+
cmd = RemoveKerningCommand(pair=('A', 'V'))
|
|
56
|
+
editor.execute(cmd, context)
|
|
57
|
+
|
|
58
|
+
# Undo/Redo
|
|
59
|
+
editor.undo()
|
|
60
|
+
editor.redo()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Margins Operations
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from ufo_spacing_lib import (
|
|
67
|
+
MarginsEditor,
|
|
68
|
+
FontContext,
|
|
69
|
+
AdjustMarginCommand,
|
|
70
|
+
SetMarginCommand,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
editor = MarginsEditor()
|
|
74
|
+
context = FontContext.from_single_font(font)
|
|
75
|
+
|
|
76
|
+
# Adjust left margin (propagates to composites by default)
|
|
77
|
+
cmd = AdjustMarginCommand(
|
|
78
|
+
glyph_name='A',
|
|
79
|
+
side='left',
|
|
80
|
+
delta=10,
|
|
81
|
+
propagate_to_composites=True
|
|
82
|
+
)
|
|
83
|
+
editor.execute(cmd, context)
|
|
84
|
+
|
|
85
|
+
# Set right margin to absolute value
|
|
86
|
+
cmd = SetMarginCommand(
|
|
87
|
+
glyph_name='A',
|
|
88
|
+
side='right',
|
|
89
|
+
value=50
|
|
90
|
+
)
|
|
91
|
+
editor.execute(cmd, context)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Multi-Font Operations (Interpolation)
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# Create context for multiple fonts with scaling
|
|
98
|
+
context = FontContext.from_linked_fonts(
|
|
99
|
+
fonts=[light_master, regular_master, bold_master],
|
|
100
|
+
primary=regular_master,
|
|
101
|
+
scales={
|
|
102
|
+
light_master: 0.8,
|
|
103
|
+
regular_master: 1.0,
|
|
104
|
+
bold_master: 1.3
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Command applies to all fonts with appropriate scaling
|
|
109
|
+
cmd = AdjustKerningCommand(pair=('A', 'V'), delta=-10)
|
|
110
|
+
editor.execute(cmd, context)
|
|
111
|
+
# light_master: -8, regular_master: -10, bold_master: -13
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Event Callbacks
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
def on_kerning_change(command, result):
|
|
118
|
+
print(f"Kerning changed: {command.description}")
|
|
119
|
+
refresh_ui()
|
|
120
|
+
|
|
121
|
+
editor.on_change = on_kerning_change
|
|
122
|
+
editor.on_undo = on_kerning_change
|
|
123
|
+
editor.on_redo = on_kerning_change
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Preview/Simulation (VirtualFont)
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from ufo_spacing_lib import VirtualFont, FontContext, AdjustKerningCommand
|
|
130
|
+
|
|
131
|
+
# Create virtual copy - isolates kerning/groups changes
|
|
132
|
+
virtual = VirtualFont.from_font(font)
|
|
133
|
+
|
|
134
|
+
# Work as usual - changes only affect virtual.kerning/groups
|
|
135
|
+
context = FontContext.from_single_font(virtual)
|
|
136
|
+
cmd = AdjustKerningCommand(pair=('A', 'V'), delta=-10)
|
|
137
|
+
editor.execute(cmd, context)
|
|
138
|
+
|
|
139
|
+
# Glyphs are live references - changes in font visible through virtual
|
|
140
|
+
print(virtual['A'].leftMargin) # Same as font['A'].leftMargin
|
|
141
|
+
|
|
142
|
+
# Check what changed
|
|
143
|
+
if virtual.has_changes():
|
|
144
|
+
for pair, (old, new) in virtual.get_kerning_diff().items():
|
|
145
|
+
print(f"{pair}: {old} -> {new}")
|
|
146
|
+
|
|
147
|
+
# Apply to real font when ready, or reset
|
|
148
|
+
virtual.apply_to(font) # Writes changes to font
|
|
149
|
+
# virtual.reset() # Discards all changes
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Architecture
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
ufo_spacing_lib/
|
|
156
|
+
├── __init__.py # Main exports
|
|
157
|
+
├── contexts.py # FontContext class
|
|
158
|
+
├── groups_core.py # FontGroupsManager, KernPairInfo, resolve_kern_pair
|
|
159
|
+
├── virtual.py # VirtualFont for preview/simulation
|
|
160
|
+
├── commands/
|
|
161
|
+
│ ├── __init__.py
|
|
162
|
+
│ ├── base.py # Command ABC, CommandResult
|
|
163
|
+
│ ├── kerning.py # Kerning commands
|
|
164
|
+
│ └── margins.py # Margins commands
|
|
165
|
+
└── editors/
|
|
166
|
+
├── __init__.py
|
|
167
|
+
├── kerning.py # KerningEditor
|
|
168
|
+
└── margins.py # MarginsEditor
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Font Object Interface
|
|
172
|
+
|
|
173
|
+
The library is designed to work with any font object that implements this interface:
|
|
174
|
+
|
|
175
|
+
### For Kerning Operations
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
class FontKerning:
|
|
179
|
+
"""Dict-like kerning access."""
|
|
180
|
+
def __getitem__(self, pair: Tuple[str, str]) -> int: ...
|
|
181
|
+
def __setitem__(self, pair: Tuple[str, str], value: int): ...
|
|
182
|
+
def __delitem__(self, pair: Tuple[str, str]): ...
|
|
183
|
+
def __contains__(self, pair: Tuple[str, str]) -> bool: ...
|
|
184
|
+
def get(self, pair: Tuple[str, str], default=None) -> Optional[int]: ...
|
|
185
|
+
|
|
186
|
+
class Font:
|
|
187
|
+
kerning: FontKerning
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### For Margins Operations
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
class Glyph:
|
|
194
|
+
leftMargin: Optional[int]
|
|
195
|
+
rightMargin: Optional[int]
|
|
196
|
+
width: int
|
|
197
|
+
components: List[Component] # Optional
|
|
198
|
+
|
|
199
|
+
def moveBy(self, delta: Tuple[int, int]): ...
|
|
200
|
+
def changed(self): ... # Optional
|
|
201
|
+
|
|
202
|
+
class Component:
|
|
203
|
+
offset: Tuple[int, int]
|
|
204
|
+
def moveBy(self, delta: Tuple[int, int]): ...
|
|
205
|
+
|
|
206
|
+
class Font:
|
|
207
|
+
def __getitem__(self, glyph_name: str) -> Glyph: ...
|
|
208
|
+
def __contains__(self, glyph_name: str) -> bool: ...
|
|
209
|
+
def getReverseComponentMapping(self) -> Dict[str, List[str]]: ... # Optional
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Commands Reference
|
|
213
|
+
|
|
214
|
+
### Kerning Commands
|
|
215
|
+
|
|
216
|
+
| Command | Description |
|
|
217
|
+
|---------|-------------|
|
|
218
|
+
| `SetKerningCommand(pair, value)` | Set kerning to absolute value |
|
|
219
|
+
| `AdjustKerningCommand(pair, delta)` | Adjust kerning by delta |
|
|
220
|
+
| `RemoveKerningCommand(pair)` | Remove a kerning pair |
|
|
221
|
+
| `CreateExceptionCommand(pair, value, side)` | Create kerning exception |
|
|
222
|
+
|
|
223
|
+
### Margins Commands
|
|
224
|
+
|
|
225
|
+
| Command | Description |
|
|
226
|
+
|---------|-------------|
|
|
227
|
+
| `SetMarginCommand(glyph, side, value)` | Set margin to absolute value |
|
|
228
|
+
| `AdjustMarginCommand(glyph, side, delta)` | Adjust margin by delta |
|
|
229
|
+
|
|
230
|
+
All commands support:
|
|
231
|
+
- Multi-font operations via `FontContext`
|
|
232
|
+
- Per-font scaling
|
|
233
|
+
- Full undo/redo
|
|
234
|
+
|
|
235
|
+
## Testing
|
|
236
|
+
|
|
237
|
+
The library includes 100+ unit tests covering all components.
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# Run all tests
|
|
241
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
242
|
+
|
|
243
|
+
# Run specific test module
|
|
244
|
+
PYTHONPATH=src python3 -m unittest tests.test_kerning_commands -v
|
|
245
|
+
|
|
246
|
+
# With pytest (if installed)
|
|
247
|
+
PYTHONPATH=src python3 -m pytest tests/ -v
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Test Coverage
|
|
251
|
+
|
|
252
|
+
| Module | Tests | Coverage |
|
|
253
|
+
|--------|-------|----------|
|
|
254
|
+
| Kerning Commands | 25 | SetKerning, AdjustKerning, RemoveKerning, CreateException |
|
|
255
|
+
| Editors | 20 | KerningEditor, MarginsEditor, undo/redo, callbacks |
|
|
256
|
+
| Groups Manager | 30 | FontGroupsManager, add/remove/delete/rename groups |
|
|
257
|
+
| VirtualFont | 27 | Creation, isolation, glyph access, diff tracking, apply/reset |
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT License
|
|
262
|
+
|
|
263
|
+
## Author
|
|
264
|
+
|
|
265
|
+
Alexander Lubovenko
|
|
266
|
+
lubovenko@gmail.com
|
|
267
|
+
github.com/typedev
|
|
268
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ufo-spacing-lib"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Font spacing management library for UFO-compatible fonts (kerning, margins, groups)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Alexander Lubovenko", email = "lubovenko@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"font",
|
|
17
|
+
"typography",
|
|
18
|
+
"kerning",
|
|
19
|
+
"spacing",
|
|
20
|
+
"ufo",
|
|
21
|
+
"fonttools",
|
|
22
|
+
"type-design"
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Topic :: Text Processing :: Fonts",
|
|
34
|
+
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
|
35
|
+
"Typing :: Typed",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=7.0",
|
|
41
|
+
"pytest-cov",
|
|
42
|
+
"ruff",
|
|
43
|
+
"mypy",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/typedev/ufo-spacing-lib"
|
|
48
|
+
Repository = "https://github.com/typedev/ufo-spacing-lib"
|
|
49
|
+
Documentation = "https://github.com/typedev/ufo-spacing-lib#readme"
|
|
50
|
+
Issues = "https://github.com/typedev/ufo-spacing-lib/issues"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.wheel]
|
|
53
|
+
packages = ["src/ufo_spacing_lib"]
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.sdist]
|
|
56
|
+
include = [
|
|
57
|
+
"/src",
|
|
58
|
+
"/tests",
|
|
59
|
+
"/README.md",
|
|
60
|
+
"/LICENSE",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
line-length = 88
|
|
65
|
+
target-version = "py310"
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint]
|
|
68
|
+
select = ["E", "F", "W", "I", "UP"]
|
|
69
|
+
ignore = ["E501"]
|
|
70
|
+
|
|
71
|
+
[tool.mypy]
|
|
72
|
+
python_version = "3.10"
|
|
73
|
+
warn_return_any = true
|
|
74
|
+
warn_unused_configs = true
|
|
75
|
+
ignore_missing_imports = true
|
|
76
|
+
|
|
77
|
+
[tool.pytest.ini_options]
|
|
78
|
+
testpaths = ["tests"]
|
|
79
|
+
python_files = "test_*.py"
|
|
80
|
+
python_functions = "test_*"
|
|
81
|
+
|