rotulo 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 @@
1
+ <svg color="#7db6e3" style="--tesela-dir:-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="48" height="48" aria-hidden="true"><path d="M5 7L5 4L12 4"/><path d="M12 4L19 4L19 7"/><path d="M12 4L12 11"/><path d="M12 11L12 17.5"/><path d="M4.29 3.29L6 5" stroke="color-mix(in oklch, currentColor, black 24%)" style="stroke:oklch(from currentColor calc(l - 0.144 * var(--tesela-dir, 1)) c h)" stroke-width="0.16" stroke-linecap="butt"/><path d="M11 5L11 3" stroke="color-mix(in oklch, currentColor, black 24%)" style="stroke:oklch(from currentColor calc(l - 0.144 * var(--tesela-dir, 1)) c h)" stroke-width="0.16" stroke-linecap="butt"/><path d="M13 3L13 5" stroke="color-mix(in oklch, currentColor, black 24%)" style="stroke:oklch(from currentColor calc(l - 0.144 * var(--tesela-dir, 1)) c h)" stroke-width="0.16" stroke-linecap="butt"/><path d="M13 5L11 5" stroke="color-mix(in oklch, currentColor, black 24%)" style="stroke:oklch(from currentColor calc(l - 0.144 * var(--tesela-dir, 1)) c h)" stroke-width="0.16" stroke-linecap="butt"/><path d="M19.71 3.29L18 5" stroke="color-mix(in oklch, currentColor, black 24%)" style="stroke:oklch(from currentColor calc(l - 0.144 * var(--tesela-dir, 1)) c h)" stroke-width="0.16" stroke-linecap="butt"/><path d="M11 11L13 11" stroke="color-mix(in oklch, currentColor, black 24%)" style="stroke:oklch(from currentColor calc(l - 0.144 * var(--tesela-dir, 1)) c h)" stroke-width="0.16" stroke-linecap="butt"/><path d="M9 20.5C11 20.5 13 20.5 15 20.5" stroke="color-mix(in oklch, currentColor, black 60%)" style="stroke:oklch(from currentColor calc(l - 0.36 * var(--tesela-dir, 1)) c h)"/></svg>
@@ -0,0 +1 @@
1
+ <svg color="#1d5d8f" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="48" height="48" aria-hidden="true"><path d="M5 7L5 4L12 4"/><path d="M12 4L19 4L19 7"/><path d="M12 4L12 11"/><path d="M12 11L12 17.5"/><path d="M4.29 3.29L6 5" stroke="color-mix(in oklch, currentColor, black 24%)" style="stroke:oklch(from currentColor calc(l - 0.144 * var(--tesela-dir, 1)) c h)" stroke-width="0.16" stroke-linecap="butt"/><path d="M11 5L11 3" stroke="color-mix(in oklch, currentColor, black 24%)" style="stroke:oklch(from currentColor calc(l - 0.144 * var(--tesela-dir, 1)) c h)" stroke-width="0.16" stroke-linecap="butt"/><path d="M13 3L13 5" stroke="color-mix(in oklch, currentColor, black 24%)" style="stroke:oklch(from currentColor calc(l - 0.144 * var(--tesela-dir, 1)) c h)" stroke-width="0.16" stroke-linecap="butt"/><path d="M13 5L11 5" stroke="color-mix(in oklch, currentColor, black 24%)" style="stroke:oklch(from currentColor calc(l - 0.144 * var(--tesela-dir, 1)) c h)" stroke-width="0.16" stroke-linecap="butt"/><path d="M19.71 3.29L18 5" stroke="color-mix(in oklch, currentColor, black 24%)" style="stroke:oklch(from currentColor calc(l - 0.144 * var(--tesela-dir, 1)) c h)" stroke-width="0.16" stroke-linecap="butt"/><path d="M11 11L13 11" stroke="color-mix(in oklch, currentColor, black 24%)" style="stroke:oklch(from currentColor calc(l - 0.144 * var(--tesela-dir, 1)) c h)" stroke-width="0.16" stroke-linecap="butt"/><path d="M9 20.5C11 20.5 13 20.5 15 20.5" stroke="color-mix(in oklch, currentColor, black 60%)" style="stroke:oklch(from currentColor calc(l - 0.36 * var(--tesela-dir, 1)) c h)"/></svg>
Binary file
@@ -0,0 +1,30 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [develop]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python-version: ['3.12', '3.13', '3.14']
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ - uses: actions/setup-python@v7
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+ cache: pip
25
+ cache-dependency-path: pyproject.toml
26
+ - run: python -m pip install -e '.[dev]'
27
+ - run: ruff check .
28
+ - run: ruff format --check .
29
+ - run: mypy
30
+ - run: pytest
@@ -0,0 +1,64 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ['v*']
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ fonts:
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ contents: write
15
+ id-token: write
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+ - uses: actions/setup-python@v7
19
+ with:
20
+ python-version: '3.12'
21
+ - name: Tag, pyproject and package.json agree
22
+ run: |
23
+ py=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
24
+ js=$(python -c "import json; print(json.load(open('web/package.json'))['version'])")
25
+ [ "v$py" = "$GITHUB_REF_NAME" ] && [ "v$js" = "$GITHUB_REF_NAME" ] || { echo "tag $GITHUB_REF_NAME, pyproject $py, package.json $js"; exit 1; }
26
+ - run: python -m pip install .
27
+ - run: python -m rotulo --out build --web web/fonts
28
+ - name: Release notes from the changelog
29
+ run: awk -v v="${GITHUB_REF_NAME#v}" 'index($0, "## [" v "]") == 1 { on = 1; next } on && /^## \[/ { exit } on' docs/CHANGELOG.md > notes.md
30
+ - name: GitHub release
31
+ env:
32
+ GH_TOKEN: ${{ github.token }}
33
+ run: |
34
+ gh release view "$GITHUB_REF_NAME" > /dev/null 2>&1 || gh release create "$GITHUB_REF_NAME" \
35
+ --title "rotulo ${GITHUB_REF_NAME#v}" --notes-file notes.md --verify-tag \
36
+ build/*.ttf web/fonts/*.woff2
37
+ - uses: actions/setup-node@v4
38
+ with:
39
+ node-version: 24
40
+ registry-url: https://registry.npmjs.org
41
+ - run: npm install -g npm@^11.5.1
42
+ - name: npm
43
+ working-directory: web
44
+ env:
45
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
46
+ run: |
47
+ cp ../README.md ../LICENSE .
48
+ name=$(node -p "require('./package.json').name")
49
+ npm view "$name@${GITHUB_REF_NAME#v}" version > /dev/null 2>&1 || npm publish --provenance --access public
50
+
51
+ pypi:
52
+ runs-on: ubuntu-latest
53
+ environment: pypi
54
+ permissions:
55
+ contents: read
56
+ id-token: write
57
+ steps:
58
+ - uses: actions/checkout@v7
59
+ - uses: actions/setup-python@v7
60
+ with:
61
+ python-version: '3.12'
62
+ - run: python -m pip install build
63
+ - run: python -m build
64
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ .venv
2
+ __pycache__
3
+ *.egg-info
4
+ dist
5
+ build
6
+ .pytest_cache
7
+ .mypy_cache
8
+ .ruff_cache
9
+ test-results
10
+ web/fonts
11
+ web/README.md
12
+ web/LICENSE
rotulo-0.1.0/LICENSE ADDED
@@ -0,0 +1,93 @@
1
+ Copyright 2026 The Rotulo Project Authors (https://github.com/GHEgar/rotulo)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ https://openfontlicense.org
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
rotulo-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.5
2
+ Name: rotulo
3
+ Version: 0.1.0
4
+ Summary: Monoline typeface generated from skeletons: strokes of lines and arcs expanded with a round pen.
5
+ Author: Edgar Villarreal
6
+ License-Expression: OFL-1.1
7
+ License-File: LICENSE
8
+ Keywords: cartography,font,map-labels,monoline,typeface
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Topic :: Text Processing :: Fonts
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.12
15
+ Requires-Dist: brotli>=1.1
16
+ Requires-Dist: fonttools>=4.50
17
+ Requires-Dist: skia-pathops>=0.8
18
+ Requires-Dist: ttfautohint-py>=0.5
19
+ Provides-Extra: dev
20
+ Requires-Dist: mypy>=1.10; extra == 'dev'
21
+ Requires-Dist: pytest>=8.0; extra == 'dev'
22
+ Requires-Dist: ruff>=0.6; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # <picture><source media="(prefers-color-scheme: dark)" srcset=".github/assets/icon-dark.svg"><img src=".github/assets/icon-light.svg" width="36" height="36" alt=""></picture> rotulo
26
+
27
+ [![npm](https://img.shields.io/npm/v/@npmegar/rotulo)](https://www.npmjs.com/package/@npmegar/rotulo) [![PyPI](https://img.shields.io/pypi/v/rotulo)](https://pypi.org/project/rotulo/) [![license](https://img.shields.io/badge/license-OFL--1.1-blue)](LICENSE)
28
+
29
+ Monoline typeface generated from skeletons: strokes of lines and arcs expanded with a round pen.
30
+
31
+ ![Rotulo Display and Rotulo Lectura specimen](.github/assets/specimen.png)
32
+
33
+ Every letter is a skeleton of straight lines and arcs drawn in code, never a hand-drawn outline. The
34
+ generator expands each skeleton with a round pen and writes TrueType and WOFF2 fonts. Weight is the
35
+ pen width, so the family is a variable font from 200 to 700. Floating details (dots, accents, the zero
36
+ slash, punctuation) are a second color layer. A reading cut, **Lectura**, adds weight, spacing and
37
+ letters that are hard to mirror.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ npm install @npmegar/rotulo
43
+ pip install rotulo
44
+ ```
45
+
46
+ | Package | What it is | Needs |
47
+ |---|---|---|
48
+ | `@npmegar/rotulo` | WOFF2 files and a stylesheet with the `@font-face` rules | A bundler that resolves CSS imports, or a plain `<link>` |
49
+ | `rotulo` | The generator: builds every font from the skeletons | Python 3.12 or later |
50
+
51
+ The TTF files are attached to each [GitHub release](https://github.com/GHEgar/rotulo/releases).
52
+
53
+ ## Quick start
54
+
55
+ ```css
56
+ @import '@npmegar/rotulo';
57
+
58
+ body {
59
+ font-family: 'Rotulo', system-ui, sans-serif;
60
+ font-weight: 200;
61
+ }
62
+
63
+ .reading {
64
+ font-family: 'Rotulo Lectura', 'Rotulo', sans-serif;
65
+ }
66
+ ```
67
+
68
+ ```bash
69
+ python -m rotulo --out build --web build/web
70
+ ```
71
+
72
+ ## Fonts
73
+
74
+ | Family | File | Weight | Use |
75
+ |---|---|---|---|
76
+ | `Rotulo` | `Rotulo-Variable.woff2`, `.ttf` | 200 to 700 | Map labels, interface, titles |
77
+ | `Rotulo` | `Rotulo-Display.ttf` | 200, static and hinted | Renderers without variable fonts, such as GeoServer or PDF libraries |
78
+ | `Rotulo Lectura` | `Rotulo-Lectura.woff2`, `.ttf` | 500, static and hinted | Long reading |
79
+
80
+ ## Color details
81
+
82
+ Dots, accents, the zero slash and punctuation sit on a color layer (COLR/CPAL). The body takes the text
83
+ color; the detail takes the palette color. Renderers without color fonts draw the whole letter in the
84
+ text color.
85
+
86
+ | Palette | Detail color | CSS |
87
+ |---|---|---|
88
+ | 0, light backgrounds | `#2f5d62` | default |
89
+ | 1, dark backgrounds | `#7fb7b5` | `font-palette: --rotulo-dark` |
90
+
91
+ Override the detail color with `@font-palette-values` and `override-colors: 0 <color>`.
92
+
93
+ ## Reading forms
94
+
95
+ `ss01` switches four letters to the forms of Lectura: `l` with a tail, `q` with a crossed tail, a curved
96
+ flag on `1` and a straight `9`.
97
+
98
+ ```css
99
+ .labels {
100
+ font-feature-settings: 'ss01';
101
+ }
102
+ ```
103
+
104
+ ## Using the fonts outside the web
105
+
106
+ | Where | How |
107
+ |---|---|
108
+ | GeoServer | Copy `Rotulo-Display.ttf` into the font directory and name `Rotulo` in the SLD `font-family` |
109
+ | PDF | Embed `Rotulo-Display.ttf` or `Rotulo-Lectura.ttf` |
110
+
111
+ ## Parameters
112
+
113
+ `Params` holds every design decision, in units of a 1000 em:
114
+
115
+ | Field | Default | What it controls |
116
+ |---|---|---|
117
+ | `weight` | 29 | Pen width |
118
+ | `x_height`, `cap_height` | 420, 700 | Body and capital heights |
119
+ | `ascender`, `descender` | 800, −260 | Shared by every letter with a tail |
120
+ | `corner` | 70 | Radius used to round every skeleton vertex |
121
+ | `gap` | 110 | Space between a stroke and a floating detail |
122
+ | `spacing`, `word_spacing` | 1.56, 1.0 | Side bearings and space width |
123
+ | `narrow_air` | 0.3 | Extra room around narrow letters such as `i`, `l`, `t` |
124
+
125
+ ```python
126
+ from dataclasses import replace
127
+ from pathlib import Path
128
+
129
+ from rotulo import DISPLAY, build_font
130
+
131
+ print(build_font(replace(DISPLAY, weight=50), "Medium", 400, Path("build")))
132
+ ```
133
+
134
+ ## Known limitations
135
+
136
+ - Latin script: Spanish, Portuguese, French and Catalan, figures and common symbols.
137
+ - The variable font keeps overlapping strokes and is not hinted; use the static files at small sizes
138
+ on Windows.
139
+ - Detail colors need COLR support. Safari, Chrome, Firefox and Windows render them; older renderers draw
140
+ one color.
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ python -m venv .venv && . .venv/bin/activate
146
+ pip install -e '.[dev]'
147
+ pytest
148
+ ruff check src tests && ruff format --check src tests
149
+ mypy
150
+ ```
151
+
152
+ The tests build every font and check heights against the skeleton, kerning, color layers, `ss01`,
153
+ hinting and that the variable font interpolates every glyph.
154
+
155
+ ## Contributing
156
+
157
+ Issues are welcome. Please open one before sending a pull request.
158
+
159
+ ## License
160
+
161
+ OFL-1.1 © GHEgar
rotulo-0.1.0/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # <picture><source media="(prefers-color-scheme: dark)" srcset=".github/assets/icon-dark.svg"><img src=".github/assets/icon-light.svg" width="36" height="36" alt=""></picture> rotulo
2
+
3
+ [![npm](https://img.shields.io/npm/v/@npmegar/rotulo)](https://www.npmjs.com/package/@npmegar/rotulo) [![PyPI](https://img.shields.io/pypi/v/rotulo)](https://pypi.org/project/rotulo/) [![license](https://img.shields.io/badge/license-OFL--1.1-blue)](LICENSE)
4
+
5
+ Monoline typeface generated from skeletons: strokes of lines and arcs expanded with a round pen.
6
+
7
+ ![Rotulo Display and Rotulo Lectura specimen](.github/assets/specimen.png)
8
+
9
+ Every letter is a skeleton of straight lines and arcs drawn in code, never a hand-drawn outline. The
10
+ generator expands each skeleton with a round pen and writes TrueType and WOFF2 fonts. Weight is the
11
+ pen width, so the family is a variable font from 200 to 700. Floating details (dots, accents, the zero
12
+ slash, punctuation) are a second color layer. A reading cut, **Lectura**, adds weight, spacing and
13
+ letters that are hard to mirror.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @npmegar/rotulo
19
+ pip install rotulo
20
+ ```
21
+
22
+ | Package | What it is | Needs |
23
+ |---|---|---|
24
+ | `@npmegar/rotulo` | WOFF2 files and a stylesheet with the `@font-face` rules | A bundler that resolves CSS imports, or a plain `<link>` |
25
+ | `rotulo` | The generator: builds every font from the skeletons | Python 3.12 or later |
26
+
27
+ The TTF files are attached to each [GitHub release](https://github.com/GHEgar/rotulo/releases).
28
+
29
+ ## Quick start
30
+
31
+ ```css
32
+ @import '@npmegar/rotulo';
33
+
34
+ body {
35
+ font-family: 'Rotulo', system-ui, sans-serif;
36
+ font-weight: 200;
37
+ }
38
+
39
+ .reading {
40
+ font-family: 'Rotulo Lectura', 'Rotulo', sans-serif;
41
+ }
42
+ ```
43
+
44
+ ```bash
45
+ python -m rotulo --out build --web build/web
46
+ ```
47
+
48
+ ## Fonts
49
+
50
+ | Family | File | Weight | Use |
51
+ |---|---|---|---|
52
+ | `Rotulo` | `Rotulo-Variable.woff2`, `.ttf` | 200 to 700 | Map labels, interface, titles |
53
+ | `Rotulo` | `Rotulo-Display.ttf` | 200, static and hinted | Renderers without variable fonts, such as GeoServer or PDF libraries |
54
+ | `Rotulo Lectura` | `Rotulo-Lectura.woff2`, `.ttf` | 500, static and hinted | Long reading |
55
+
56
+ ## Color details
57
+
58
+ Dots, accents, the zero slash and punctuation sit on a color layer (COLR/CPAL). The body takes the text
59
+ color; the detail takes the palette color. Renderers without color fonts draw the whole letter in the
60
+ text color.
61
+
62
+ | Palette | Detail color | CSS |
63
+ |---|---|---|
64
+ | 0, light backgrounds | `#2f5d62` | default |
65
+ | 1, dark backgrounds | `#7fb7b5` | `font-palette: --rotulo-dark` |
66
+
67
+ Override the detail color with `@font-palette-values` and `override-colors: 0 <color>`.
68
+
69
+ ## Reading forms
70
+
71
+ `ss01` switches four letters to the forms of Lectura: `l` with a tail, `q` with a crossed tail, a curved
72
+ flag on `1` and a straight `9`.
73
+
74
+ ```css
75
+ .labels {
76
+ font-feature-settings: 'ss01';
77
+ }
78
+ ```
79
+
80
+ ## Using the fonts outside the web
81
+
82
+ | Where | How |
83
+ |---|---|
84
+ | GeoServer | Copy `Rotulo-Display.ttf` into the font directory and name `Rotulo` in the SLD `font-family` |
85
+ | PDF | Embed `Rotulo-Display.ttf` or `Rotulo-Lectura.ttf` |
86
+
87
+ ## Parameters
88
+
89
+ `Params` holds every design decision, in units of a 1000 em:
90
+
91
+ | Field | Default | What it controls |
92
+ |---|---|---|
93
+ | `weight` | 29 | Pen width |
94
+ | `x_height`, `cap_height` | 420, 700 | Body and capital heights |
95
+ | `ascender`, `descender` | 800, −260 | Shared by every letter with a tail |
96
+ | `corner` | 70 | Radius used to round every skeleton vertex |
97
+ | `gap` | 110 | Space between a stroke and a floating detail |
98
+ | `spacing`, `word_spacing` | 1.56, 1.0 | Side bearings and space width |
99
+ | `narrow_air` | 0.3 | Extra room around narrow letters such as `i`, `l`, `t` |
100
+
101
+ ```python
102
+ from dataclasses import replace
103
+ from pathlib import Path
104
+
105
+ from rotulo import DISPLAY, build_font
106
+
107
+ print(build_font(replace(DISPLAY, weight=50), "Medium", 400, Path("build")))
108
+ ```
109
+
110
+ ## Known limitations
111
+
112
+ - Latin script: Spanish, Portuguese, French and Catalan, figures and common symbols.
113
+ - The variable font keeps overlapping strokes and is not hinted; use the static files at small sizes
114
+ on Windows.
115
+ - Detail colors need COLR support. Safari, Chrome, Firefox and Windows render them; older renderers draw
116
+ one color.
117
+
118
+ ## Development
119
+
120
+ ```bash
121
+ python -m venv .venv && . .venv/bin/activate
122
+ pip install -e '.[dev]'
123
+ pytest
124
+ ruff check src tests && ruff format --check src tests
125
+ mypy
126
+ ```
127
+
128
+ The tests build every font and check heights against the skeleton, kerning, color layers, `ss01`,
129
+ hinting and that the variable font interpolates every glyph.
130
+
131
+ ## Contributing
132
+
133
+ Issues are welcome. Please open one before sending a pull request.
134
+
135
+ ## License
136
+
137
+ OFL-1.1 © GHEgar
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ Formato basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/) y versionado con
4
+ [SemVer](https://semver.org/lang/es/).
5
+
6
+ ## [0.1.0] - 2026-10-15
7
+
8
+ ### Agregado
9
+
10
+ - Generador de la tipografía desde esqueletos de rectas y arcos, expandidos con un trazo redondo.
11
+ - Fuente variable de peso de 200 a 700, con un trazador de cápsulas de estructura fija.
12
+ - Estilos estáticos Display (peso 29) y Lectura (peso 75, más espacio y letras difíciles de reflejar),
13
+ con hinting.
14
+ - Detalle a color con capas COLR y paletas para fondo claro y oscuro.
15
+ - Formas de lectura en `ss01`: `l` con cola, `q` con barra, `1` con bandera curva y `9` recto.
16
+ - Latín para español, portugués, francés y catalán, cifras, signos, comillas y kerning.
17
+ - Paquete npm `@npmegar/rotulo` con WOFF2 y las reglas `@font-face`.
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "rotulo"
7
+ version = "0.1.0"
8
+ description = "Monoline typeface generated from skeletons: strokes of lines and arcs expanded with a round pen."
9
+ readme = "README.md"
10
+ license = "OFL-1.1"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.12"
13
+ authors = [{ name = "Edgar Villarreal" }]
14
+ keywords = ["font", "typeface", "monoline", "cartography", "map-labels"]
15
+ classifiers = [
16
+ "Development Status :: 2 - Pre-Alpha",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Topic :: Text Processing :: Fonts",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = ["brotli>=1.1", "fonttools>=4.50", "skia-pathops>=0.8", "ttfautohint-py>=0.5"]
23
+
24
+ [project.optional-dependencies]
25
+ dev = ["mypy>=1.10", "pytest>=8.0", "ruff>=0.6"]
26
+
27
+ [project.scripts]
28
+ rotulo = "rotulo.__main__:main"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/rotulo"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
35
+ addopts = "-q"
36
+
37
+ [tool.ruff]
38
+ line-length = 100
39
+ target-version = "py312"
40
+ src = ["src", "tests"]
41
+ extend-exclude = ["CLAUDE.md", "AGENTS.md"]
42
+
43
+ [tool.ruff.lint]
44
+ select = ["E", "W", "F", "I", "N", "UP", "B", "SIM", "RUF", "ANN"]
45
+ ignore = ["RUF009", "ANN401"]
46
+
47
+ [tool.mypy]
48
+ strict = true
49
+ files = ["src", "tests"]
50
+ mypy_path = "src"
51
+
52
+ [[tool.mypy.overrides]]
53
+ module = ["fontTools.*", "pathops.*", "ttfautohint.*"]
54
+ ignore_missing_imports = true
@@ -0,0 +1,4 @@
1
+ from rotulo.build import build_context, build_font
2
+ from rotulo.params import DISPLAY, READING, Params
3
+
4
+ __all__ = ["DISPLAY", "READING", "Params", "build_context", "build_font"]
@@ -0,0 +1,26 @@
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ from rotulo.build import build_font
5
+ from rotulo.params import DISPLAY, READING
6
+ from rotulo.variable import build_variable
7
+ from rotulo.web import to_woff2
8
+
9
+
10
+ def main() -> None:
11
+ parser = argparse.ArgumentParser(prog="rotulo", description="Build the Rotulo fonts.")
12
+ parser.add_argument("--out", type=Path, default=Path("build"))
13
+ parser.add_argument("--no-variable", action="store_true", help="skip the variable font")
14
+ parser.add_argument("--web", type=Path, help="also write WOFF2 files into this directory")
15
+ args = parser.parse_args()
16
+ for params, style, weight_class in ((DISPLAY, "Display", 200), (READING, "Lectura", 500)):
17
+ print(build_font(params, style, weight_class, args.out))
18
+ if not args.no_variable:
19
+ print(build_variable(args.out))
20
+ if args.web:
21
+ for path in to_woff2(args.out, args.web):
22
+ print(path)
23
+
24
+
25
+ if __name__ == "__main__":
26
+ main()
@@ -0,0 +1,50 @@
1
+ from rotulo.chars import DOTLESS_I
2
+ from rotulo.context import Context
3
+ from rotulo.skeleton import Stroke
4
+
5
+
6
+ def add_accents(ctx: Context) -> None:
7
+ g, xh, pen, sh = ctx.glyphs, ctx.xh, ctx.pen, ctx.sharp
8
+
9
+ def compose(ch: str, base: str, extra: list[Stroke]) -> None:
10
+ b = g[base]
11
+ ctx.define(ch, b.width, b.left, b.right, b.strokes, [*b.details, *extra], b.ax)
12
+
13
+ def grave(x: float, y: float) -> Stroke:
14
+ return pen().m(x + xh * 0.06, y).l(x - xh * 0.08, y + xh * 0.18)
15
+
16
+ def circumflex(x: float, y: float) -> Stroke:
17
+ return pen(sharp=sh).m(x - xh * 0.12, y).l(x, y + xh * 0.16).l(x + xh * 0.12, y)
18
+
19
+ def cedilla(x: float) -> Stroke:
20
+ y0, r = -(ctx.w + ctx.gap * 0.5), xh * 0.08
21
+ return pen().m(x, y0).a(x, y0 - r, r, r, 90, -150)
22
+
23
+ marks = {
24
+ "acute": ctx.acute,
25
+ "grave": grave,
26
+ "circumflex": circumflex,
27
+ }
28
+ lower = {"a": "a", "e": "e", "i": DOTLESS_I, "o": "o", "u": "u"}
29
+ upper = {"A": "A", "E": "E", "I": "I", "O": "O", "U": "U"}
30
+ table = {
31
+ "acute": ("áéíóú", "ÁÉÍÓÚ"),
32
+ "grave": ("àèìòù", "ÀÈÌÒÙ"),
33
+ "circumflex": ("âêîôû", "ÂÊÎÔÛ"),
34
+ }
35
+ for mark, (low, up) in table.items():
36
+ draw = marks[mark]
37
+ for ch, base in zip(low, lower.values(), strict=True):
38
+ compose(ch, base, [draw(0 if base == DOTLESS_I else g[base].ax, ctx.yt)])
39
+ for ch, base in zip(up, upper.values(), strict=True):
40
+ compose(ch, base, [draw(g[base].ax, ctx.yc)])
41
+ for ch, base in (("ñ", "n"), ("ã", "a"), ("õ", "o")):
42
+ compose(ch, base, [ctx.tilde(g[base].ax, ctx.yt)])
43
+ for ch, base in (("Ñ", "N"), ("Ã", "A"), ("Õ", "O")):
44
+ compose(ch, base, [ctx.tilde(g[base].ax, ctx.yc)])
45
+ for ch, base in (("ü", "u"), ("ë", "e"), ("ï", DOTLESS_I), ("ÿ", "y")):
46
+ compose(ch, base, ctx.dieresis(0 if base == DOTLESS_I else g[base].ax, ctx.yt))
47
+ for ch, base in (("Ü", "U"), ("Ë", "E"), ("Ï", "I")):
48
+ compose(ch, base, ctx.dieresis(g[base].ax, ctx.yc))
49
+ compose("ç", "c", [cedilla(g["c"].ax)])
50
+ compose("Ç", "C", [cedilla(g["C"].ax)])