vtt-transfer 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.
@@ -0,0 +1,128 @@
1
+ .DS_Store
2
+ # Byte-compiled / optimized / DLL files
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+
7
+ # C extensions
8
+ *.so
9
+
10
+ # Distribution / packaging
11
+ .Python
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ pip-wheel-metadata/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ # PyInstaller
32
+ # Usually these files are written by a python script from a template
33
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
34
+ *.manifest
35
+ *.spec
36
+
37
+ # Installer logs
38
+ pip-log.txt
39
+ pip-delete-this-directory.txt
40
+
41
+ # Unit test / coverage reports
42
+ htmlcov/
43
+ .tox/
44
+ .nox/
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *.cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # celery beat schedule file
95
+ celerybeat-schedule
96
+
97
+ # SageMath parsed files
98
+ *.sage.py
99
+
100
+ # Environments
101
+ .env
102
+ .venv
103
+ env/
104
+ venv/
105
+ ENV/
106
+ env.bak/
107
+ venv.bak/
108
+
109
+ # Spyder project settings
110
+ .spyderproject
111
+ .spyproject
112
+
113
+ # Rope project settings
114
+ .ropeproject
115
+
116
+ # mkdocs documentation
117
+ /site
118
+
119
+ # mypy
120
+ .mypy_cache/
121
+ .dmypy.json
122
+ dmypy.json
123
+
124
+ # Pyre type checker
125
+ .pyre/
126
+
127
+ # Local working area (font builds, reports)
128
+ /work/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 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.
@@ -0,0 +1,11 @@
1
+ # Third-party notices
2
+
3
+ ## Microsoft VisualTrueType
4
+
5
+ https://github.com/microsoft/VisualTrueType, MIT License, Copyright (c) Microsoft Corporation.
6
+
7
+ - `src/vtt_transfer/talk.py` ports the VTT Talk scanner, argument matching and command table of
8
+ `src/TMTParser.cpp`.
9
+ - `src/vtt_transfer/control.py` ports the Control Program parser of `src/CvtManager.cpp`.
10
+ - `src/vtt_transfer/templates/fpgmtmpl.txt`, `cvttmpl.txt` and `chargrp.txt` are copied unchanged from
11
+ `templates/` (see `src/vtt_transfer/templates/NOTICE.md`, which carries the full license text).
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.5
2
+ Name: vtt-transfer
3
+ Version: 0.1.0
4
+ Summary: Transfer VTT (Visual TrueType) hinting sources to a new version of a TrueType font, with an HTML report of what is left to hint
5
+ Project-URL: Homepage, https://github.com/typedev/vtt-transfer
6
+ Project-URL: Repository, https://github.com/typedev/vtt-transfer
7
+ Project-URL: Issues, https://github.com/typedev/vtt-transfer/issues
8
+ Author: Alexander Lubovenko
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ License-File: NOTICE.md
12
+ Keywords: fonts,hinting,truetype,variable fonts,visual truetype,vtt
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Multimedia :: Graphics
23
+ Classifier: Topic :: Text Processing :: Fonts
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: brotli>=1.1
26
+ Requires-Dist: fonttools>=4.60
27
+ Requires-Dist: skia-pathops>=0.8
28
+ Requires-Dist: vttcompilepy>=0.0.1.13
29
+ Description-Content-Type: text/markdown
30
+
31
+ # vtt-transfer
32
+
33
+ Transfers VTT hinting sources from a hinted version of a TrueType font (static or variable) to a new version of the same font and writes an HTML report: what was transferred, what needs extra hinting, what to check.
34
+
35
+ ## Installation
36
+
37
+ ```sh
38
+ uv tool install vtt-transfer # or: pipx install vtt-transfer
39
+ ```
40
+
41
+ This puts `vtt-transfer`, `vtt-var2static` and `vtt-ship` on the PATH. Python 3.11 or newer; the VTT
42
+ compiler comes from [vttcompilepy](https://pypi.org/project/vttcompilepy/) wheels (Windows, macOS,
43
+ Linux x86_64).
44
+
45
+ From a checkout (`-e` picks up code changes without reinstalling):
46
+
47
+ ```sh
48
+ uv tool install -e /path/to/vtt-transfer
49
+ # or only the project's local environment:
50
+ cd /path/to/vtt-transfer && uv sync
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ ```sh
56
+ vtt-transfer OLD.VTT.ttf NEW.ttf # after installation
57
+ uv run vtt-transfer OLD.VTT.ttf NEW.ttf # from the vtt-transfer folder
58
+ uv run --project /path/to/vtt-transfer vtt-transfer OLD.VTT.ttf NEW.ttf # from any folder
59
+ ```
60
+
61
+ Next to `NEW.ttf` you get:
62
+
63
+ - `NEW.VTT.ttf` — the new font with VTT sources (TSI0–TSI3, TSI5), `cvt`, `fpgm`, `prep`, `gasp` and `cvar`/`TSIC` (if present), compiled with vttcompilepy. It opens in VTT and work continues as usual.
64
+ - `NEW.vtt-transfer-report.html` — the report.
65
+
66
+ Options: `-o` output path, `--report` report path, `--report-lang en|ru` report language (default `ru`), `--no-compile` skip compilation, `--no-remap` no point remapping, `-q`.
67
+
68
+ Exit code: 0 — all good, 1 — verification errors (see the report), 2 — transfer impossible (no TSI tables, different UPM).
69
+
70
+ ## How glyphs are matched
71
+
72
+ 1. **By name.** If a glyph is gone, a rename is looked for: the same Unicode and the same point structure, or a single new glyph with identical points.
73
+ 2. **Structure unchanged** (contours, points, on/off-curve, number of components): VTT Talk, assembly and bytecode are carried over as is. The GID in the headers is updated.
74
+ 3. **Structure changed:** the reference points of the VTT Talk are looked up in the new outline by exact default-master coordinates; phantom points move with the outline. If all are found, the numbers are rewritten (status "remapped"); otherwise the hints are removed, the glyph goes to "Re-hint", and the old VTT Talk is attached in the report. VTT Talk is parsed with the command signatures of Microsoft's compiler, so every command whose point arguments are typed is remapped, points of vector overrides (`YLink(0,4:9>18)`) included; commands with untyped arguments (Call, Inline/ASM, BeginCodePath, AutoHinter) and the obsolete ones the compiler refuses (Tail, DiagSerifs…) are manual work.
75
+ **Composite decomposed into a simple glyph** (e.g. after overlap removal): the VTT Talk is assembled from the components' talk, with point numbers shifted by each component's position in the flattened point list, and then remapped the same way. A component's phantom point is kept only if it lands exactly on a phantom point of the glyph (taking the component offset into account); otherwise it is lost. Scaled or flipped components are not reused. The composite's own instructions are lost and flagged.
76
+ 4. **Composites:** the assembly code (`OFFSET`/`SOFFSET`) is always regenerated from the new `glyf` (GIDs, offsets, transformations, USE_MY_METRICS), with prefixes (`OVERLAP[]` etc.) as in the source. The composite's own instructions are kept only if the point structure of its components did not change.
77
+ 5. **New simple glyphs:** if the points exactly match a hinted old glyph, all hints are copied; if they are shifted along X only (combining accents made from spacing ones), only Y commands are copied. The rest get a character group by analogy with glyphs of the same Unicode category in the source.
78
+ 6. **New composites** are hinted if all their components are; the report shows which components they are waiting for.
79
+
80
+ ## Checks
81
+
82
+ After compilation the tool verifies the result against the new font: outlines and composites match, and tables unrelated to hinting (`gvar`, `GSUB`, `GPOS`…) have unchanged content. A compile error in a single glyph removes its hints and goes to the report; the rest is compiled.
83
+
84
+ ## Limitations
85
+
86
+ - **Build the new font with the same compiler and options as the hinted source.** fontc drops implied on-curve points (exactly halfway between two off-curve points), fontmake keeps them unless `--drop-implied-oncurves` is passed. On one family a fontc build broke 14 hinted glyphs (`O`, `Q`, `©`, `®`, `°`…) and their composites that a fontmake build of the same sources transferred cleanly. The report flags reference points lost this way and warns when many glyphs are hit. See [doc/compilers.md](https://github.com/typedev/vtt-transfer/blob/master/doc/compilers.md).
87
+ - Remapping uses default-master coordinates: a point moved by even one unit is considered lost. This is intentional.
88
+ - Composite code with point attachment (`ANCHOR`) is generated but flagged for manual review.
89
+ - VOLT tables (TSIV etc.) are not transferred.
90
+
91
+ ## Autohinter project (.hprj)
92
+
93
+ If a VTThinter project lies next to the source (`family.hprj` for `family.VTT.ttf`) or is passed with `--hprj`, an updated copy (`NEW.hprj`) is written next to the result. The original file is not touched; `--no-hprj` disables the update.
94
+
95
+ - Each class is matched to a TSI5 group by the majority of its glyphs (for example: UpperCase → 2, LowerCase → 3, Figures → 4, Superiors → 1, Parens → 8, Accents → 9).
96
+ - Renamed glyphs are replaced in the lists in place, removed ones are dropped.
97
+ - New simple glyphs are added to the class of their group (the group is assigned the same way as in TSI5). Group 0 glyphs are not put into classes. Glyphs that became composites stay in the list and are flagged in the report.
98
+ - `TTFFileName`/`XMLFileName` are rewritten to the new font's name if it differs.
99
+ - Only these values change; formatting and line endings (CRLF) are preserved.
100
+
101
+ For a static cut by `vtt-var2static`, the VF's project (`family-vf.hprj` next to the VF, or `--hprj`) is
102
+ copied as `<name>.hprj` with `TTFFileName`/`XMLFileName` set to the static and every `cvt`/`value` pair
103
+ set to the static's CVT (the same as the VF's without `cvar`; a value that already disagreed with the
104
+ VF's CVT is corrected and printed). The glyph lists stay as they are. The stem range of each class is
105
+ checked against the strokes measured on the static (the thickest straight or round stroke, by the
106
+ reference glyphs of `vtt_transfer.measure`: H I L / E, n m h u / z, 1 / 2 4 7, O o 0); a class outside
107
+ its `stem min`–`max` is printed as a warning — bold statics need a wider range than the VF's default
108
+ master. Classes without reference glyphs (superiors, parens, accents) are not measured.
109
+
110
+ ## Statics cut from a hinted VF: vtt-var2static
111
+
112
+ ```sh
113
+ vtt-var2static path/family-vf.VTT.ttf [-o DIR] [-i "Condensed Light" -i Bold] [--keep-overlaps] [--ship [--woff2]]
114
+ ```
115
+
116
+ Cuts every named instance of a hinted variable font (or only the ones given with `-i`: subfamily or
117
+ file name, case and spaces ignored) into `DIR/<name>/` (default `statics/` next to the font):
118
+
119
+ `<name>` is the font's file name without `-vf`/`_vf` plus the instance's subfamily without spaces
120
+ (`family-vf.VTT.ttf`, `Condensed Light` → `family-CondensedLight`); an italic VF's `_italic`/`-italic`
121
+ is dropped when the subfamily says Italic, so `family_italic-vf` gives `family-BoldItalic`.
122
+
123
+ - `<name>.overlap.VTT.ttf` — the instance with overlaps and VTT sources (the transfer source);
124
+ - `<name>.ttf` — overlaps removed, hinting stripped (the transfer target);
125
+ - `<name>.VTT.ttf` — the target with the hints transferred and compiled; open it in VTT;
126
+ - `<name>.vtt-transfer-report.html` — the transfer report: what was remapped, what needs re-hinting;
127
+ - `<name>.hprj` — the VF's VTThinter project for this static, if the VF has one (see below).
128
+
129
+ `--keep-overlaps` skips overlap removal: the instance keeps its hints as they are and is written as
130
+ `<name>.VTT.ttf`, with nothing to transfer. `--ship` also writes release fonts without VTT sources into
131
+ `DIR/hinted/` (as `vtt-ship`, checked against `<name>.ttf`), `--woff2` adds `.woff2`. Options
132
+ `--report-lang`, `-q`, `--hprj`, `--no-hprj` as in `vtt-transfer`. Exit code: 0 — all good, 1 — some instance failed (cut,
133
+ transfer verification or ship check), 2 — the font has no VTT sources or no named instances.
134
+
135
+ ### Library
136
+
137
+ `vtt_transfer.static` cuts the named instances of a hinted variable font
138
+ (`doc/static-instances-from-vf.md`): `named_instances(vf, family)` lists them, `cut(vf_path, instance,
139
+ out_dir, remove_overlaps=True)` writes `<name>.overlap.VTT.ttf` (the instance with overlaps and VTT
140
+ sources — the transfer source) and, with overlap removal, `<name>.ttf` (overlaps removed, hinting
141
+ stripped — the transfer target). Overlap removal can turn a hinted on-curve point into an implied one
142
+ (exactly between its off-curve neighbours); `cut` puts such points back, so the outline is the same and
143
+ the remap finds them. Transferring source → target carries the hints over by exact remap.
144
+ `vtt-var2static` runs it for every instance. Needs `skia-pathops`.
145
+
146
+ ## VTT sources from scratch (library)
147
+
148
+ `vtt_transfer.fresh.prepare(font)` gives an unhinted static TrueType font VTT sources from Microsoft's
149
+ templates (`src/vtt_transfer/templates/`, MIT, from microsoft/VisualTrueType — see `NOTICE.md`), as VTT's
150
+ "Prepare Font" + "Control Program Only" would: glyf instructions and `fpgm`/`prep`/`cvt` dropped; the
151
+ standard font program; the Control Program template with the values of `vtt_transfer.measure` written
152
+ into its CVT lines (`2: 690 /* cap height */ /* measured: H E F … (17/17) */`, unmeasured slots marked);
153
+ character groups (TSI5) from `chargrp.txt` by Unicode, unencoded glyphs by their base name, else by Unicode
154
+ category; empty Talk, composites with `OFFSET[R]` assembly. The compiler generates the pre-program from
155
+ the Control Program. Returns the compiled font and a report (measurements, group counts).
156
+
157
+ `vtt_transfer.measure.measure(font)`: heights from the dominant cluster of reference glyph
158
+ tops/bottoms (Latin + Cyrillic; overshoots relative to their height), stems from scanline ink runs, kept
159
+ apart by group (UpperCase / LowerCase / Figure), direction and straight/round, keyed by the template's
160
+ CVT numbers. Diagonals, serifs and italic CVTs are not
161
+ measured.
162
+
163
+ For re-measuring one CVT from glyphs the operator picks: `measure_height(font, glyphs, "top"|"bottom")`,
164
+ `measure_overshoot(font, glyphs, side, parent_value)` (round − flat) and `measure_stem(font, glyphs, axis,
165
+ where, band=…)` take a string of characters (through the cmap) or a list of glyph names and return
166
+ `{value, samples, total, glyphs, values, missing}` (`values`: each glyph's samples, to spot the one that
167
+ disagrees; an empty or unknown set gives `value` None). `default_glyphs(cvt)` gives the set and
168
+ parameters `measure` uses for a template CVT, None for the ones it does not measure.
169
+
170
+ ## Control Program (library)
171
+
172
+ `vtt_transfer.control` parses and edits the VTT Control Program (TSI1 `cvt` source). It is a port of
173
+ the compiler's grammar (VisualTrueType `CvtManager.cpp`). Parsing is token based: attributes stay set
174
+ until changed, comments nest, and indentation means nothing. `parse(text)` returns a `Program` with
175
+ the GROUP/CATEGORY declarations (TSI5 group numbers), the settings, the `ASM` statements, every CVT
176
+ (value, `~`/`=` inheritance, break ppem, deltas, attributes, the first two comments of its line,
177
+ absolute value) and the errors with their lines. It never raises. `set_cvt(text, n, value=…, ppem=…,
178
+ deltas=…, name=…, note=…)` and `set_setting(text, key, value)` change only the edited tokens and keep
179
+ comments, spacing and line ends. `add_cvt` places a new CVT next to the CVTs with the same attributes
180
+ (heights get the first free number below 40, others the first free from 65) and gives a square height
181
+ the rounding pair (`ASM("SVTCA[Y]")`, `ASM("CALL[], n, 89")`) when the program does that for its other
182
+ heights. `remove_cvt` removes a CVT with its comments and rounding pair, and refuses a CVT that is
183
+ another CVT's parent; it does not look at glyph Talk. xscale and fresh are built on it.
184
+
185
+ ## X-height scaling (library)
186
+
187
+ `vtt_transfer.xscale` adds ttfautohint-style x-height scaling to a VTT-hinted font, in its sources. VTT
188
+ rounds every height CVT on its own, so at a size where the x-height rounds up the caps may round down;
189
+ ttfautohint rounds the x-height (with overshoot) and scales all heights by the same factor before
190
+ rounding. `apply(font, Params(mode="both"|"up", limit=14, maxppem=0))` adds two functions to the fpgm
191
+ source and one `ASM("CALL[] …")` line in the Control Program before the first height CVT, so the
192
+ scaling runs in the pre-program before VTT's own rounding. `find_heights` reads the height CVTs
193
+ (SquareHeight / RoundHeight; the x-height is the LowerCase one equal to `OS/2.sxHeight`), `detect`
194
+ reports the state of the scaling in the sources (including a call made stale by CVT edits), `remove`
195
+ takes it out. Only TSI1 is edited; the caller compiles.
196
+
197
+ ## Release fonts: vtt-ship
198
+
199
+ ```sh
200
+ vtt-ship path/family-vf.VTT.ttf path/family-italic-vf.VTT.ttf [--woff2] # or uv run vtt-ship …
201
+ ```
202
+
203
+ Compiles the VTT sources (vttcompilepy, the same compiler as in VTT) and writes a font without the TSI* tables: hints, `cvt`, `fpgm`, `prep`, `gasp` stay. The result goes to a `hinted/` subfolder next to the source, named without `.VTT` (`-o DIR` to write elsewhere).
204
+
205
+ Before writing, the result is checked against the designer's unhinted build (by default `<name without .VTT>.ttf` next to it, or `--reference`): outlines, composites with offsets and all tables unrelated to hinting must match. This catches stale composite code in VTT (VTT builds composites from the OFFSET lines in the sources, not from `glyf`). On a mismatch or a compile error the file is not written (`--force` to write anyway, `--no-check` to skip the check). Exit code 1 if any font failed.
206
+
207
+ ## Development
208
+
209
+ ```sh
210
+ uv sync
211
+ uv run python tests/smoke.py # end-to-end run on a synthetic font, as in CI
212
+ ```
213
+
214
+ ## License
215
+
216
+ MIT, see [LICENSE](https://github.com/typedev/vtt-transfer/blob/master/LICENSE). Parts are ported from or copied out of Microsoft's
217
+ [VisualTrueType](https://github.com/microsoft/VisualTrueType) (MIT), see [NOTICE.md](https://github.com/typedev/vtt-transfer/blob/master/NOTICE.md).
@@ -0,0 +1,187 @@
1
+ # vtt-transfer
2
+
3
+ Transfers VTT hinting sources from a hinted version of a TrueType font (static or variable) to a new version of the same font and writes an HTML report: what was transferred, what needs extra hinting, what to check.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ uv tool install vtt-transfer # or: pipx install vtt-transfer
9
+ ```
10
+
11
+ This puts `vtt-transfer`, `vtt-var2static` and `vtt-ship` on the PATH. Python 3.11 or newer; the VTT
12
+ compiler comes from [vttcompilepy](https://pypi.org/project/vttcompilepy/) wheels (Windows, macOS,
13
+ Linux x86_64).
14
+
15
+ From a checkout (`-e` picks up code changes without reinstalling):
16
+
17
+ ```sh
18
+ uv tool install -e /path/to/vtt-transfer
19
+ # or only the project's local environment:
20
+ cd /path/to/vtt-transfer && uv sync
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```sh
26
+ vtt-transfer OLD.VTT.ttf NEW.ttf # after installation
27
+ uv run vtt-transfer OLD.VTT.ttf NEW.ttf # from the vtt-transfer folder
28
+ uv run --project /path/to/vtt-transfer vtt-transfer OLD.VTT.ttf NEW.ttf # from any folder
29
+ ```
30
+
31
+ Next to `NEW.ttf` you get:
32
+
33
+ - `NEW.VTT.ttf` — the new font with VTT sources (TSI0–TSI3, TSI5), `cvt`, `fpgm`, `prep`, `gasp` and `cvar`/`TSIC` (if present), compiled with vttcompilepy. It opens in VTT and work continues as usual.
34
+ - `NEW.vtt-transfer-report.html` — the report.
35
+
36
+ Options: `-o` output path, `--report` report path, `--report-lang en|ru` report language (default `ru`), `--no-compile` skip compilation, `--no-remap` no point remapping, `-q`.
37
+
38
+ Exit code: 0 — all good, 1 — verification errors (see the report), 2 — transfer impossible (no TSI tables, different UPM).
39
+
40
+ ## How glyphs are matched
41
+
42
+ 1. **By name.** If a glyph is gone, a rename is looked for: the same Unicode and the same point structure, or a single new glyph with identical points.
43
+ 2. **Structure unchanged** (contours, points, on/off-curve, number of components): VTT Talk, assembly and bytecode are carried over as is. The GID in the headers is updated.
44
+ 3. **Structure changed:** the reference points of the VTT Talk are looked up in the new outline by exact default-master coordinates; phantom points move with the outline. If all are found, the numbers are rewritten (status "remapped"); otherwise the hints are removed, the glyph goes to "Re-hint", and the old VTT Talk is attached in the report. VTT Talk is parsed with the command signatures of Microsoft's compiler, so every command whose point arguments are typed is remapped, points of vector overrides (`YLink(0,4:9>18)`) included; commands with untyped arguments (Call, Inline/ASM, BeginCodePath, AutoHinter) and the obsolete ones the compiler refuses (Tail, DiagSerifs…) are manual work.
45
+ **Composite decomposed into a simple glyph** (e.g. after overlap removal): the VTT Talk is assembled from the components' talk, with point numbers shifted by each component's position in the flattened point list, and then remapped the same way. A component's phantom point is kept only if it lands exactly on a phantom point of the glyph (taking the component offset into account); otherwise it is lost. Scaled or flipped components are not reused. The composite's own instructions are lost and flagged.
46
+ 4. **Composites:** the assembly code (`OFFSET`/`SOFFSET`) is always regenerated from the new `glyf` (GIDs, offsets, transformations, USE_MY_METRICS), with prefixes (`OVERLAP[]` etc.) as in the source. The composite's own instructions are kept only if the point structure of its components did not change.
47
+ 5. **New simple glyphs:** if the points exactly match a hinted old glyph, all hints are copied; if they are shifted along X only (combining accents made from spacing ones), only Y commands are copied. The rest get a character group by analogy with glyphs of the same Unicode category in the source.
48
+ 6. **New composites** are hinted if all their components are; the report shows which components they are waiting for.
49
+
50
+ ## Checks
51
+
52
+ After compilation the tool verifies the result against the new font: outlines and composites match, and tables unrelated to hinting (`gvar`, `GSUB`, `GPOS`…) have unchanged content. A compile error in a single glyph removes its hints and goes to the report; the rest is compiled.
53
+
54
+ ## Limitations
55
+
56
+ - **Build the new font with the same compiler and options as the hinted source.** fontc drops implied on-curve points (exactly halfway between two off-curve points), fontmake keeps them unless `--drop-implied-oncurves` is passed. On one family a fontc build broke 14 hinted glyphs (`O`, `Q`, `©`, `®`, `°`…) and their composites that a fontmake build of the same sources transferred cleanly. The report flags reference points lost this way and warns when many glyphs are hit. See [doc/compilers.md](https://github.com/typedev/vtt-transfer/blob/master/doc/compilers.md).
57
+ - Remapping uses default-master coordinates: a point moved by even one unit is considered lost. This is intentional.
58
+ - Composite code with point attachment (`ANCHOR`) is generated but flagged for manual review.
59
+ - VOLT tables (TSIV etc.) are not transferred.
60
+
61
+ ## Autohinter project (.hprj)
62
+
63
+ If a VTThinter project lies next to the source (`family.hprj` for `family.VTT.ttf`) or is passed with `--hprj`, an updated copy (`NEW.hprj`) is written next to the result. The original file is not touched; `--no-hprj` disables the update.
64
+
65
+ - Each class is matched to a TSI5 group by the majority of its glyphs (for example: UpperCase → 2, LowerCase → 3, Figures → 4, Superiors → 1, Parens → 8, Accents → 9).
66
+ - Renamed glyphs are replaced in the lists in place, removed ones are dropped.
67
+ - New simple glyphs are added to the class of their group (the group is assigned the same way as in TSI5). Group 0 glyphs are not put into classes. Glyphs that became composites stay in the list and are flagged in the report.
68
+ - `TTFFileName`/`XMLFileName` are rewritten to the new font's name if it differs.
69
+ - Only these values change; formatting and line endings (CRLF) are preserved.
70
+
71
+ For a static cut by `vtt-var2static`, the VF's project (`family-vf.hprj` next to the VF, or `--hprj`) is
72
+ copied as `<name>.hprj` with `TTFFileName`/`XMLFileName` set to the static and every `cvt`/`value` pair
73
+ set to the static's CVT (the same as the VF's without `cvar`; a value that already disagreed with the
74
+ VF's CVT is corrected and printed). The glyph lists stay as they are. The stem range of each class is
75
+ checked against the strokes measured on the static (the thickest straight or round stroke, by the
76
+ reference glyphs of `vtt_transfer.measure`: H I L / E, n m h u / z, 1 / 2 4 7, O o 0); a class outside
77
+ its `stem min`–`max` is printed as a warning — bold statics need a wider range than the VF's default
78
+ master. Classes without reference glyphs (superiors, parens, accents) are not measured.
79
+
80
+ ## Statics cut from a hinted VF: vtt-var2static
81
+
82
+ ```sh
83
+ vtt-var2static path/family-vf.VTT.ttf [-o DIR] [-i "Condensed Light" -i Bold] [--keep-overlaps] [--ship [--woff2]]
84
+ ```
85
+
86
+ Cuts every named instance of a hinted variable font (or only the ones given with `-i`: subfamily or
87
+ file name, case and spaces ignored) into `DIR/<name>/` (default `statics/` next to the font):
88
+
89
+ `<name>` is the font's file name without `-vf`/`_vf` plus the instance's subfamily without spaces
90
+ (`family-vf.VTT.ttf`, `Condensed Light` → `family-CondensedLight`); an italic VF's `_italic`/`-italic`
91
+ is dropped when the subfamily says Italic, so `family_italic-vf` gives `family-BoldItalic`.
92
+
93
+ - `<name>.overlap.VTT.ttf` — the instance with overlaps and VTT sources (the transfer source);
94
+ - `<name>.ttf` — overlaps removed, hinting stripped (the transfer target);
95
+ - `<name>.VTT.ttf` — the target with the hints transferred and compiled; open it in VTT;
96
+ - `<name>.vtt-transfer-report.html` — the transfer report: what was remapped, what needs re-hinting;
97
+ - `<name>.hprj` — the VF's VTThinter project for this static, if the VF has one (see below).
98
+
99
+ `--keep-overlaps` skips overlap removal: the instance keeps its hints as they are and is written as
100
+ `<name>.VTT.ttf`, with nothing to transfer. `--ship` also writes release fonts without VTT sources into
101
+ `DIR/hinted/` (as `vtt-ship`, checked against `<name>.ttf`), `--woff2` adds `.woff2`. Options
102
+ `--report-lang`, `-q`, `--hprj`, `--no-hprj` as in `vtt-transfer`. Exit code: 0 — all good, 1 — some instance failed (cut,
103
+ transfer verification or ship check), 2 — the font has no VTT sources or no named instances.
104
+
105
+ ### Library
106
+
107
+ `vtt_transfer.static` cuts the named instances of a hinted variable font
108
+ (`doc/static-instances-from-vf.md`): `named_instances(vf, family)` lists them, `cut(vf_path, instance,
109
+ out_dir, remove_overlaps=True)` writes `<name>.overlap.VTT.ttf` (the instance with overlaps and VTT
110
+ sources — the transfer source) and, with overlap removal, `<name>.ttf` (overlaps removed, hinting
111
+ stripped — the transfer target). Overlap removal can turn a hinted on-curve point into an implied one
112
+ (exactly between its off-curve neighbours); `cut` puts such points back, so the outline is the same and
113
+ the remap finds them. Transferring source → target carries the hints over by exact remap.
114
+ `vtt-var2static` runs it for every instance. Needs `skia-pathops`.
115
+
116
+ ## VTT sources from scratch (library)
117
+
118
+ `vtt_transfer.fresh.prepare(font)` gives an unhinted static TrueType font VTT sources from Microsoft's
119
+ templates (`src/vtt_transfer/templates/`, MIT, from microsoft/VisualTrueType — see `NOTICE.md`), as VTT's
120
+ "Prepare Font" + "Control Program Only" would: glyf instructions and `fpgm`/`prep`/`cvt` dropped; the
121
+ standard font program; the Control Program template with the values of `vtt_transfer.measure` written
122
+ into its CVT lines (`2: 690 /* cap height */ /* measured: H E F … (17/17) */`, unmeasured slots marked);
123
+ character groups (TSI5) from `chargrp.txt` by Unicode, unencoded glyphs by their base name, else by Unicode
124
+ category; empty Talk, composites with `OFFSET[R]` assembly. The compiler generates the pre-program from
125
+ the Control Program. Returns the compiled font and a report (measurements, group counts).
126
+
127
+ `vtt_transfer.measure.measure(font)`: heights from the dominant cluster of reference glyph
128
+ tops/bottoms (Latin + Cyrillic; overshoots relative to their height), stems from scanline ink runs, kept
129
+ apart by group (UpperCase / LowerCase / Figure), direction and straight/round, keyed by the template's
130
+ CVT numbers. Diagonals, serifs and italic CVTs are not
131
+ measured.
132
+
133
+ For re-measuring one CVT from glyphs the operator picks: `measure_height(font, glyphs, "top"|"bottom")`,
134
+ `measure_overshoot(font, glyphs, side, parent_value)` (round − flat) and `measure_stem(font, glyphs, axis,
135
+ where, band=…)` take a string of characters (through the cmap) or a list of glyph names and return
136
+ `{value, samples, total, glyphs, values, missing}` (`values`: each glyph's samples, to spot the one that
137
+ disagrees; an empty or unknown set gives `value` None). `default_glyphs(cvt)` gives the set and
138
+ parameters `measure` uses for a template CVT, None for the ones it does not measure.
139
+
140
+ ## Control Program (library)
141
+
142
+ `vtt_transfer.control` parses and edits the VTT Control Program (TSI1 `cvt` source). It is a port of
143
+ the compiler's grammar (VisualTrueType `CvtManager.cpp`). Parsing is token based: attributes stay set
144
+ until changed, comments nest, and indentation means nothing. `parse(text)` returns a `Program` with
145
+ the GROUP/CATEGORY declarations (TSI5 group numbers), the settings, the `ASM` statements, every CVT
146
+ (value, `~`/`=` inheritance, break ppem, deltas, attributes, the first two comments of its line,
147
+ absolute value) and the errors with their lines. It never raises. `set_cvt(text, n, value=…, ppem=…,
148
+ deltas=…, name=…, note=…)` and `set_setting(text, key, value)` change only the edited tokens and keep
149
+ comments, spacing and line ends. `add_cvt` places a new CVT next to the CVTs with the same attributes
150
+ (heights get the first free number below 40, others the first free from 65) and gives a square height
151
+ the rounding pair (`ASM("SVTCA[Y]")`, `ASM("CALL[], n, 89")`) when the program does that for its other
152
+ heights. `remove_cvt` removes a CVT with its comments and rounding pair, and refuses a CVT that is
153
+ another CVT's parent; it does not look at glyph Talk. xscale and fresh are built on it.
154
+
155
+ ## X-height scaling (library)
156
+
157
+ `vtt_transfer.xscale` adds ttfautohint-style x-height scaling to a VTT-hinted font, in its sources. VTT
158
+ rounds every height CVT on its own, so at a size where the x-height rounds up the caps may round down;
159
+ ttfautohint rounds the x-height (with overshoot) and scales all heights by the same factor before
160
+ rounding. `apply(font, Params(mode="both"|"up", limit=14, maxppem=0))` adds two functions to the fpgm
161
+ source and one `ASM("CALL[] …")` line in the Control Program before the first height CVT, so the
162
+ scaling runs in the pre-program before VTT's own rounding. `find_heights` reads the height CVTs
163
+ (SquareHeight / RoundHeight; the x-height is the LowerCase one equal to `OS/2.sxHeight`), `detect`
164
+ reports the state of the scaling in the sources (including a call made stale by CVT edits), `remove`
165
+ takes it out. Only TSI1 is edited; the caller compiles.
166
+
167
+ ## Release fonts: vtt-ship
168
+
169
+ ```sh
170
+ vtt-ship path/family-vf.VTT.ttf path/family-italic-vf.VTT.ttf [--woff2] # or uv run vtt-ship …
171
+ ```
172
+
173
+ Compiles the VTT sources (vttcompilepy, the same compiler as in VTT) and writes a font without the TSI* tables: hints, `cvt`, `fpgm`, `prep`, `gasp` stay. The result goes to a `hinted/` subfolder next to the source, named without `.VTT` (`-o DIR` to write elsewhere).
174
+
175
+ Before writing, the result is checked against the designer's unhinted build (by default `<name without .VTT>.ttf` next to it, or `--reference`): outlines, composites with offsets and all tables unrelated to hinting must match. This catches stale composite code in VTT (VTT builds composites from the OFFSET lines in the sources, not from `glyf`). On a mismatch or a compile error the file is not written (`--force` to write anyway, `--no-check` to skip the check). Exit code 1 if any font failed.
176
+
177
+ ## Development
178
+
179
+ ```sh
180
+ uv sync
181
+ uv run python tests/smoke.py # end-to-end run on a synthetic font, as in CI
182
+ ```
183
+
184
+ ## License
185
+
186
+ MIT, see [LICENSE](https://github.com/typedev/vtt-transfer/blob/master/LICENSE). Parts are ported from or copied out of Microsoft's
187
+ [VisualTrueType](https://github.com/microsoft/VisualTrueType) (MIT), see [NOTICE.md](https://github.com/typedev/vtt-transfer/blob/master/NOTICE.md).
@@ -0,0 +1,69 @@
1
+ # Build the new version with the same compiler as the hinted source
2
+
3
+ Status: observed on a hinted variable sans family, 2026-10-11.
4
+
5
+ ## Summary
6
+
7
+ VTT hints refer to point numbers. A compiler that changes the point structure of glyphs whose
8
+ design did not change makes the transfer lose those glyphs, and every composite built from them.
9
+
10
+ - **fontc** dropped *implied on-curve points* in this build: an on-curve point lying exactly halfway
11
+ between two off-curve points (in all masters) can be omitted from `glyf`, since the rasterizer
12
+ implies it.
13
+ - **fontmake** (ufo2ft / fontTools varLib) keeps them. Dropping is opt-in there:
14
+ `fontmake --drop-implied-oncurves` (TrueType only, default off). The fontTools option was added,
15
+ per its PR, for diffing fontmake output against fontc.
16
+
17
+ Rule: build every version of a hinted family with the same compiler and the same point-related
18
+ options as the font the VTT sources were made on. For our projects so far that is
19
+ fontmake with default options (no `--drop-implied-oncurves`). Record the compiler in the project's
20
+ build profile, and treat a compiler switch like an outline change of the whole font.
21
+
22
+ ## Evidence
23
+
24
+ Source: the family's hinted VF (built with fontmake), 340 hinted simple glyphs. Target:
25
+ the same designspace built twice on 2026-10-11 — with `variableCompiler: fontc` in the build
26
+ profile, and again after that option was removed (fontmake).
27
+
28
+ | | fontc build | fontmake build |
29
+ |---|---|---|
30
+ | hinted simple glyphs with the source point structure | 320 | 334 |
31
+ | transferred | 527 | 563 |
32
+ | remapped | 2 | 0 |
33
+ | re-hint | 16 | 4 |
34
+ | changed, unhinted | 28 | 0 |
35
+ | composite lost own instructions | 22 | 0 |
36
+
37
+ The builds are about an hour apart with design commits in between, so not every difference in the
38
+ lower rows is the compiler. The structural comparison isolates it: **14 hinted glyphs differ from
39
+ the source only in the fontc build, and only by dropped implied on-curve points**:
40
+
41
+ `O` (27), `Q` (37), `copyright` (0 5 10 15 20 30), `registered` (10 20 28),
42
+ `degree` (0 4 8 12 19 25), `paragraph` (5), `Oslash` (39), `uni042E` (35), `uni0472` (31),
43
+ `uni221E` (4 20), `uniF406` (0), `uniF6D1` (8), `uni2117` (0 5 10 15 20 30),
44
+ `uni2300` (0 4 8 12 16 20 24 28) — source point numbers in parentheses.
45
+
46
+ - In 13 of them the dropped points are implied at all 18 named instances of the source as well.
47
+ - In 12 of them a dropped point is referenced by the VTT Talk — e.g. `O`: `YShift(9,27)` moves the
48
+ top of the counter, and point 27 no longer exists — so the transfer cannot remap them (re-hint).
49
+ - In `paragraph` the dropped point (5) is not referenced: the remap succeeds, but all later point
50
+ numbers shift by one.
51
+ - Composites inherit the change: `Odieresis`, `Oacute`… lose their own instructions because the
52
+ point structure of their component `O` changed.
53
+
54
+ The other 6 hinted glyphs that differ from the source in both builds are design changes (e.g.
55
+ Greek `uni0394`, `uni03A9`, `uni03C0` redrawn), not the compiler.
56
+
57
+ ## Recognising it in a report
58
+
59
+ A lost reference point that was an on-curve point exactly at the midpoint of its two off-curve
60
+ neighbours in the source, with the neighbours still present in the target, is almost certainly a
61
+ dropped implied on-curve. Many such glyphs in one transfer (round shapes: `O`, `Q`, `©`, `®`, `°`)
62
+ point at the build, not the design.
63
+
64
+ The transfer recognises this (`core.implied_oncurve`): such a lost point gets the reason "gone as an
65
+ implied on-curve between off-curve points a and b", the glyph card says how many of its lost points
66
+ these are, and a font-level check lists the glyphs (a warning from 3 glyphs on). The remap stays
67
+ exact: the point does not exist in the target, so the glyph still goes to re-hint. The same check
68
+ fires on statics cut before `static.restore_implied_oncurves` existed, where overlap removal dropped
69
+ the points.