Skip to content

Commit 4a0378e

Browse files
committed
badge-maker: migrate to pretext for measurement
1 parent 8949c46 commit 4a0378e

File tree

10 files changed

+990
-27
lines changed

10 files changed

+990
-27
lines changed

.github/workflows/test-package-lib.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,15 @@ jobs:
3434
env:
3535
NPM_CONFIG_ENGINE_STRICT: ${{ matrix.engine-strict }}
3636

37+
- name: Install fonts for text measurement
38+
run: |
39+
echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" | sudo debconf-set-selections
40+
sudo apt-get update
41+
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
42+
ttf-mscorefonts-installer
43+
44+
- name: Download fonts
45+
run: node scripts/download-fonts.js
46+
3747
- name: Package tests
3848
uses: ./.github/actions/package-tests

badge-maker/fonts/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Exclude all font files from version control.
2+
# Verdana is proprietary (© Microsoft) and cannot be committed.
3+
# All fonts are downloaded/copied by `node scripts/download-fonts.js`.
4+
*.ttf
5+
*.otf

badge-maker/fonts/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# badge-maker/fonts/
2+
3+
Runtime fonts for badge text-width measurement.
4+
5+
## Setup
6+
7+
Populate this directory before running badge-maker tests:
8+
9+
```sh
10+
node scripts/download-fonts.js
11+
```
12+
13+
The script downloads free fonts from GitHub and copies Verdana from your system (see below).
14+
15+
## Fonts
16+
17+
| File | Family | License | Notes |
18+
| --- | --- | --- | --- |
19+
| `DejaVuSans.ttf` | DejaVu Sans | [Bitstream Vera](https://dejavu-fonts.github.io/License.html) | Free to redistribute |
20+
| `DejaVuSans-Bold.ttf` | DejaVu Sans | Bitstream Vera | Free to redistribute |
21+
| `LiberationSans-Regular.ttf` | Liberation Sans | [SIL OFL 1.1](https://scripts.sil.org/OFL) | Free to redistribute; metric-compatible with Arial/Helvetica |
22+
| `LiberationSans-Bold.ttf` | Liberation Sans | SIL OFL 1.1 | Free to redistribute |
23+
| `Verdana.ttf` | Verdana | © Microsoft | **Proprietary — do NOT commit** |
24+
| `Verdana_Bold.ttf` | Verdana | © Microsoft | **Proprietary — do NOT commit** |
25+
26+
## .gitignore
27+
28+
Verdana is excluded from version control (see `badge-maker/.gitignore`). The free fonts (DejaVu Sans, Liberation Sans) are also excluded since they are re-downloaded by `scripts/download-fonts.js` during CI setup.
29+
30+
## Why Liberation Sans for Helvetica?
31+
32+
The social badge style measures text with `bold 11px Helvetica`. Helvetica is not freely distributable and is not available on most Linux systems. Liberation Sans is metric-compatible with Arial and Helvetica as specified in the [Liberation Fonts README](https://github.com/liberationfonts/liberation-fonts); registering it under both family names in `canvas-polyfill.js` gives identical glyph widths on all platforms.

badge-maker/lib/badge-renderers.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import anafanafo from 'anafanafo'
1+
import './canvas-polyfill.js'
2+
import { prepareWithSegments, walkLineRanges } from '@chenglou/pretext'
23
import { brightness } from './color.js'
34
import { XmlElement, ElementList } from './xml.js'
45

@@ -27,9 +28,18 @@ function roundUpToOdd(val) {
2728
return val % 2 === 0 ? val + 1 : val
2829
}
2930

31+
function measureTextWidth(str, font) {
32+
const prepared = prepareWithSegments(str, font)
33+
let width = 0
34+
walkLineRanges(prepared, Infinity, line => {
35+
width = line.width
36+
})
37+
return width
38+
}
39+
3040
function preferredWidthOf(str, options) {
3141
// Increase chances of pixel grid alignment.
32-
return roundUpToOdd(anafanafo(str, options) | 0)
42+
return roundUpToOdd(measureTextWidth(str, options.font) | 0)
3343
}
3444

3545
function createAccessibleText({ label, message }) {
@@ -787,11 +797,11 @@ function forTheBadge({
787797
// the discrepancy. Ideally, swapping out `textLength` for `letterSpacing`
788798
// should not affect the appearance.
789799
const labelTextWidth = label.length
790-
? (anafanafo(label, { font: `${FONT_SIZE}px Verdana` }) | 0) +
800+
? (measureTextWidth(label, `${FONT_SIZE}px Verdana`) | 0) +
791801
LETTER_SPACING * label.length
792802
: 0
793803
const messageTextWidth = message.length
794-
? (anafanafo(message, { font: `bold ${FONT_SIZE}px Verdana` }) | 0) +
804+
? (measureTextWidth(message, `bold ${FONT_SIZE}px Verdana`) | 0) +
795805
LETTER_SPACING * message.length
796806
: 0
797807

badge-maker/lib/canvas-polyfill.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Polyfill OffscreenCanvas for Node.js environments where it's not available.
2+
// Required by @chenglou/pretext which uses canvas measureText for text width.
3+
if (typeof globalThis.OffscreenCanvas === 'undefined') {
4+
try {
5+
const { createCanvas, GlobalFonts } = await import('@napi-rs/canvas')
6+
const { existsSync } = await import('fs')
7+
const { fileURLToPath } = await import('url')
8+
const { join, dirname } = await import('path')
9+
10+
// @napi-rs/canvas (Skia) does not automatically load system fonts on Linux.
11+
// Fonts are bundled in badge-maker/fonts/ (run `node scripts/download-fonts.js`
12+
// to populate the directory before running tests).
13+
const fontsDir = join(
14+
dirname(fileURLToPath(import.meta.url)),
15+
'..',
16+
'fonts',
17+
)
18+
19+
// [filename, familyNameOverride]
20+
// familyNameOverride replaces the font's own internal family name in the
21+
// Skia registry, so that CSS font strings like 'bold 11px Helvetica' resolve
22+
// to the bundled metric-compatible substitute (Liberation Sans).
23+
const fonts = [
24+
// Verdana — flat / plastic / for-the-badge measurement font
25+
['Verdana.ttf'],
26+
['Verdana_Bold.ttf'],
27+
// Liberation Sans registered as Helvetica for social-badge measurement
28+
['LiberationSans-Regular.ttf', 'Helvetica'],
29+
['LiberationSans-Bold.ttf', 'Helvetica'],
30+
// Liberation Sans also registered as Arial (font-family fallback in SVG)
31+
['LiberationSans-Regular.ttf', 'Arial'],
32+
['LiberationSans-Bold.ttf', 'Arial'],
33+
// DejaVu Sans — fallback in badge font-family lists
34+
['DejaVuSans.ttf'],
35+
['DejaVuSans-Bold.ttf'],
36+
]
37+
38+
for (const [filename, familyName] of fonts) {
39+
const fontPath = join(fontsDir, filename)
40+
if (existsSync(fontPath)) {
41+
GlobalFonts.registerFromPath(fontPath, familyName)
42+
}
43+
}
44+
45+
globalThis.OffscreenCanvas = class OffscreenCanvas {
46+
constructor(width, height) {
47+
this._canvas = createCanvas(width, height)
48+
}
49+
50+
getContext(type) {
51+
return this._canvas.getContext(type)
52+
}
53+
}
54+
} catch {
55+
// @napi-rs/canvas not available; OffscreenCanvas must be provided by the runtime
56+
}
57+
}

badge-maker/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"logo": "https://opencollective.com/opencollective/logo.txt"
4242
},
4343
"dependencies": {
44-
"anafanafo": "2.0.0",
44+
"@chenglou/pretext": "0.0.3",
45+
"@napi-rs/canvas": "^0.1.97",
4546
"css-color-converter": "^2.0.0"
4647
},
4748
"scripts": {

0 commit comments

Comments
 (0)