Skip to content

Commit 2d6b981

Browse files
committed
badge-maker: migrate to pretext for measurement
1 parent 7599611 commit 2d6b981

File tree

11 files changed

+2784
-27
lines changed

11 files changed

+2784
-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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# badge-maker/fonts/
2+
3+
Runtime fonts for badge text-width measurement.
4+
5+
`@chenglou/pretext` measures text width through canvas text rendering, so the computed width depends on the fonts that are actually available at runtime. That means badge rendering is only reproducible if CI and local development use the same font files.
6+
7+
## Setup
8+
9+
Populate this directory before running badge-maker tests:
10+
11+
```sh
12+
node scripts/download-fonts.js
13+
```
14+
15+
The script downloads free fonts from GitHub and copies Verdana from your system (see below).
16+
17+
This setup is required for two reasons:
18+
19+
1. CI machines may not have the fonts needed by `pretext` installed.
20+
2. Developers should run `pretext` against the same fonts as CI so text-width calculation stays consistent across environments and matches the committed snapshots.
21+
22+
## Fonts
23+
24+
| File | Family | License | Notes |
25+
| --- | --- | --- | --- |
26+
| `DejaVuSans.ttf` | DejaVu Sans | [Bitstream Vera](https://dejavu-fonts.github.io/License.html) | Free to redistribute |
27+
| `DejaVuSans-Bold.ttf` | DejaVu Sans | Bitstream Vera | Free to redistribute |
28+
| `LiberationSans-Regular.ttf` | Liberation Sans | [SIL OFL 1.1](https://scripts.sil.org/OFL) | Free to redistribute; metric-compatible with Arial/Helvetica |
29+
| `LiberationSans-Bold.ttf` | Liberation Sans | SIL OFL 1.1 | Free to redistribute |
30+
| `Verdana.ttf` | Verdana | © Microsoft | **Proprietary — do NOT commit** |
31+
| `Verdana_Bold.ttf` | Verdana | © Microsoft | **Proprietary — do NOT commit** |
32+
33+
## .gitignore
34+
35+
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.
36+
37+
In other words, this directory is part of the runtime contract for text measurement: `pretext` needs these fonts to produce stable widths in both CI and local development.
38+
39+
## Why Liberation Sans for Helvetica?
40+
41+
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)