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.
- rotulo-0.1.0/.github/assets/icon-dark.svg +1 -0
- rotulo-0.1.0/.github/assets/icon-light.svg +1 -0
- rotulo-0.1.0/.github/assets/specimen.png +0 -0
- rotulo-0.1.0/.github/workflows/ci.yml +30 -0
- rotulo-0.1.0/.github/workflows/release.yml +64 -0
- rotulo-0.1.0/.gitignore +12 -0
- rotulo-0.1.0/LICENSE +93 -0
- rotulo-0.1.0/PKG-INFO +161 -0
- rotulo-0.1.0/README.md +137 -0
- rotulo-0.1.0/docs/CHANGELOG.md +17 -0
- rotulo-0.1.0/pyproject.toml +54 -0
- rotulo-0.1.0/src/rotulo/__init__.py +4 -0
- rotulo-0.1.0/src/rotulo/__main__.py +26 -0
- rotulo-0.1.0/src/rotulo/accents.py +50 -0
- rotulo-0.1.0/src/rotulo/build.py +97 -0
- rotulo-0.1.0/src/rotulo/capsule.py +104 -0
- rotulo-0.1.0/src/rotulo/chars.py +13 -0
- rotulo-0.1.0/src/rotulo/context.py +142 -0
- rotulo-0.1.0/src/rotulo/figures.py +217 -0
- rotulo-0.1.0/src/rotulo/glyphset.py +112 -0
- rotulo-0.1.0/src/rotulo/lowercase.py +280 -0
- rotulo-0.1.0/src/rotulo/outline.py +45 -0
- rotulo-0.1.0/src/rotulo/params.py +51 -0
- rotulo-0.1.0/src/rotulo/py.typed +0 -0
- rotulo-0.1.0/src/rotulo/skeleton.py +164 -0
- rotulo-0.1.0/src/rotulo/spacing.py +81 -0
- rotulo-0.1.0/src/rotulo/symbols.py +259 -0
- rotulo-0.1.0/src/rotulo/uppercase.py +212 -0
- rotulo-0.1.0/src/rotulo/variable.py +77 -0
- rotulo-0.1.0/src/rotulo/web.py +23 -0
- rotulo-0.1.0/tests/__init__.py +0 -0
- rotulo-0.1.0/tests/test_build.py +63 -0
- rotulo-0.1.0/tests/test_skeleton.py +40 -0
- rotulo-0.1.0/web/package.json +34 -0
- rotulo-0.1.0/web/rotulo.css +25 -0
|
@@ -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
|
rotulo-0.1.0/.gitignore
ADDED
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
|
+
[](https://www.npmjs.com/package/@npmegar/rotulo) [](https://pypi.org/project/rotulo/) [](LICENSE)
|
|
28
|
+
|
|
29
|
+
Monoline typeface generated from skeletons: strokes of lines and arcs expanded with a round pen.
|
|
30
|
+
|
|
31
|
+

|
|
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
|
+
[](https://www.npmjs.com/package/@npmegar/rotulo) [](https://pypi.org/project/rotulo/) [](LICENSE)
|
|
4
|
+
|
|
5
|
+
Monoline typeface generated from skeletons: strokes of lines and arcs expanded with a round pen.
|
|
6
|
+
|
|
7
|
+

|
|
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,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)])
|