Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 221 additions & 61 deletions server/api/registry/badge/[type]/[...pkg].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,75 +51,235 @@ const SHIELDS_FONT_SHORTHAND = 'normal normal 400 11px Verdana, Geneva, DejaVu S

let cachedCanvasContext: SKRSContext2D | null | undefined

const NARROW_CHARS = new Set([' ', '!', '"', "'", '(', ')', '*', ',', '-', '.', ':', ';', '|'])
const MEDIUM_CHARS = new Set([
'#',
'$',
'+',
'/',
'<',
'=',
'>',
'?',
'@',
'[',
'\\',
']',
'^',
'_',
'`',
'{',
'}',
'~',
])

const FALLBACK_WIDTHS = {
const CHAR_WIDTHS: Record<'default' | 'shieldsio', Record<string, number>> = {
/**
* Manually measured widths for font locally via:
*
* ```ts
* // Geist font widths
* const ctx = createCanvas(1, 1).getContext('2d')
* const chars = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
* ctx.font = BADGE_FONT_SHORTHAND
* const entries = [...chars].map(ch => `'${ch === "'" ? "\\'" : ch === '\\' ? '\\\\' : ch}': ${Math.ceil(ctx.measureText(ch).width)}`)
* console.log('default: {\n ' + entries.join(', ') + '\n}')
*
* // Verdana font widths
* ctx.font = SHIELDS_FONT_SHORTHAND
* const entries = [...chars].map(ch => `'${ch === "'" ? "\\'" : ch === '\\' ? '\\\\' : ch}': ${Math.ceil(ctx.measureText(ch).width)}`)
* console.log('shieldsio: {\n ' + entries.join(', ') + '\n}')
* ```
*/
default: {
narrow: 3,
medium: 5,
digit: 6,
uppercase: 7,
other: 6,
' ': 3,
'!': 3,
'"': 4,
'#': 6,
'$': 6,
'%': 10,
'&': 7,
"'": 3,
'(': 4,
')': 4,
'*': 5,
'+': 6,
',': 3,
'-': 5,
'.': 3,
'/': 4,
':': 4,
';': 4,
'<': 7,
'=': 6,
'>': 7,
'?': 5,
'@': 10,
'[': 4,
'\\': 4,
']': 4,
'^': 6,
'_': 4,
'`': 6,
'{': 4,
'|': 4,
'}': 4,
'~': 6,
'0': 7,
'1': 4,
'2': 6,
'3': 6,
'4': 7,
'5': 6,
'6': 7,
'7': 6,
'8': 7,
'9': 7,
'A': 7,
'B': 7,
'C': 7,
'D': 8,
'E': 7,
'F': 6,
'G': 7,
'H': 8,
'I': 3,
'J': 5,
'K': 7,
'L': 6,
'M': 9,
'N': 8,
'O': 8,
'P': 7,
'Q': 8,
'R': 7,
'S': 6,
'T': 6,
'U': 8,
'V': 7,
'W': 10,
'X': 7,
'Y': 7,
'Z': 6,
'a': 6,
'b': 6,
'c': 5,
'd': 6,
'e': 6,
'f': 4,
'g': 6,
'h': 6,
'i': 3,
'j': 3,
'k': 6,
'l': 3,
'm': 9,
'n': 6,
'o': 6,
'p': 6,
'q': 6,
'r': 4,
's': 5,
't': 4,
'u': 6,
'v': 6,
'w': 9,
'x': 6,
'y': 6,
'z': 5,
},
shieldsio: {
narrow: 3,
medium: 5,
digit: 6,
uppercase: 7,
other: 5.5,
' ': 4,
'!': 5,
'"': 6,
'#': 9,
'$': 7,
'%': 12,
'&': 8,
"'": 3,
'(': 5,
')': 5,
'*': 7,
'+': 9,
',': 4,
'-': 5,
'.': 4,
'/': 5,
':': 5,
';': 5,
'<': 9,
'=': 9,
'>': 9,
'?': 6,
'@': 11,
'[': 5,
'\\': 5,
']': 5,
'^': 9,
'_': 7,
'`': 7,
'{': 7,
'|': 5,
'}': 7,
'~': 9,
'0': 7,
'1': 7,
'2': 7,
'3': 7,
'4': 7,
'5': 7,
'6': 7,
'7': 7,
'8': 7,
'9': 7,
'A': 8,
'B': 8,
'C': 8,
'D': 9,
'E': 7,
'F': 7,
'G': 9,
'H': 9,
'I': 5,
'J': 5,
'K': 8,
'L': 7,
'M': 10,
'N': 9,
'O': 9,
'P': 7,
'Q': 9,
'R': 8,
'S': 8,
'T': 7,
'U': 9,
'V': 8,
'W': 11,
'X': 8,
'Y': 7,
'Z': 8,
'a': 7,
'b': 7,
'c': 6,
'd': 7,
'e': 7,
'f': 4,
'g': 7,
'h': 7,
'i': 4,
'j': 4,
'k': 7,
'l': 4,
'm': 11,
'n': 7,
'o': 7,
'p': 7,
'q': 7,
'r': 5,
's': 6,
't': 5,
'u': 7,
'v': 7,
'w': 9,
'x': 7,
'y': 7,
'z': 6,
},
} as const

function estimateTextWidth(text: string, fallbackFont: 'default' | 'shieldsio'): number {
// Heuristic coefficients tuned to keep fallback rendering close to canvas metrics.
const widths = FALLBACK_WIDTHS[fallbackFont]
let totalWidth = 0

for (const character of text) {
if (NARROW_CHARS.has(character)) {
totalWidth += widths.narrow
continue
}

if (MEDIUM_CHARS.has(character)) {
totalWidth += widths.medium
continue
}

if (/\d/.test(character)) {
totalWidth += widths.digit
continue
}
}
// Fallback advance width for any character not in the lookup table above, e.g. emojis, CJK, etc.
const CHAR_WIDTH_FALLBACK: Record<'default' | 'shieldsio', number> = {
default: 12,
shieldsio: 8,
}

if (/[A-Z]/.test(character)) {
totalWidth += widths.uppercase
continue
}
function estimateTextWidth(text: string, font: 'default' | 'shieldsio'): number {
const table = CHAR_WIDTHS[font]
const fallback = CHAR_WIDTH_FALLBACK[font]
let total = 0

totalWidth += widths.other
for (const ch of text) {
total += table[ch] ?? fallback
}

return Math.max(1, Math.round(totalWidth))
return Math.max(1, Math.round(total))
}

function getCanvasContext(): SKRSContext2D | null {
Expand Down
Loading