Skip to content

fix: use lookup table for badges char widths when canvas not available#2487

Open
t128n wants to merge 2 commits intonpmx-dev:mainfrom
t128n:fix/text-measurement-without-canvas-api
Open

fix: use lookup table for badges char widths when canvas not available#2487
t128n wants to merge 2 commits intonpmx-dev:mainfrom
t128n:fix/text-measurement-without-canvas-api

Conversation

@t128n
Copy link
Copy Markdown
Contributor

@t128n t128n commented Apr 12, 2026

🔗 Linked issue

Resolves #1601

🧭 Context

In production, measuring text with canvas is crashing, resulting in inaccurate fallback measurements:

overflowing badge

Introduced a character lookup table of "manually" measured character widths for more
accurate lookups.

📚 Description

Tweaked the estimateTextWidth function to use a on-character level lookup table for more accurate results when the Canvas API from @napi-rs/canvas is unavailable.
Lookup table derived from running

// ...
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}')

locally, where fonts and the API are available.

Using a generous fallback for characters that aren't in the map, such as emojis.

📷 Comparison

Overflow / Long Labels

Case Prod PR
Long label
Long value
Long package name

Shields.io Style (?style=shieldsio)

Case Prod PR
Normal
Long label
Emoji label

Emojis

Case Prod PR
Emoji in label
Emoji in value
Multiple emojis
Emoji-only label

CJK Characters

Case Prod PR
Chinese label
Japanese label
Korean label
Mixed CJK + ASCII

Special & Punctuation Characters

Case Prod PR
Symbols
Narrow chars (iiiii)
Wide chars (WWWWW)
Mixed widths

Badge Types Smoke Test

Type Prod PR
version
license
downloads
size
types
deprecated

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 12, 2026 0:18am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 12, 2026 0:18am
npmx-lunaria Ignored Ignored Apr 12, 2026 0:18am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

Replaced heuristic character-width estimation using character sets and fallback width buckets with static per-character lookup tables (CHAR_WIDTHS) for supported badge fonts. Updated estimateTextWidth function signature and logic to compute width via direct table lookups with a fallback constant for unmapped characters.

Changes

Cohort / File(s) Summary
Badge Text-Width Measurement
server/api/registry/badge/[type]/[...pkg].get.ts
Replaced character categorisation logic (narrow/medium/digit/uppercase/other buckets) with per-character lookup tables for default and shieldsio fonts. Added CHAR_WIDTH_FALLBACK constant for unmapped characters (emojis, CJK). Refactored estimateTextWidth to accept font parameter and sum character widths from lookup table.

Possibly related PRs

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: use lookup table for badges char widths when canvas not available' clearly and specifically describes the main change: replacing canvas-based text measurement with a character lookup table approach.
Linked Issues check ✅ Passed The PR directly addresses issue #1601 by implementing accurate character-width lookups to replace failing Canvas API calls, ensuring reliable badge rendering on Vercel with proper handling of ASCII, emojis, CJK characters, and punctuation.
Out of Scope Changes check ✅ Passed All changes remain within scope: replacing the character-width estimation logic with a lookup table approach in the badge route module, with no unrelated modifications to other areas.
Description check ✅ Passed The pull request description clearly relates to the changeset, explaining the motivation (Canvas API crashes), the solution (character lookup table), and providing extensive visual comparisons demonstrating the fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 12, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@t128n t128n changed the title fix(badges): use lookup table for char widths when canvas not available fix: use lookup table for badges char widths when canvas not available Apr 12, 2026
@t128n t128n marked this pull request as ready for review April 12, 2026 12:15
@serhalp serhalp added needs review This PR is waiting for a review from a maintainer ux Related to wider UX decisions labels Apr 12, 2026
@ghostdevv ghostdevv self-requested a review April 13, 2026 18:31
Copy link
Copy Markdown
Contributor

@ghostdevv ghostdevv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't break on the last test case from #2199 so we can yolo this :p

Copy link
Copy Markdown
Contributor

@ghostdevv ghostdevv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, rq why is it "when canvas not available"? @t128n

@t128n
Copy link
Copy Markdown
Contributor Author

t128n commented Apr 14, 2026

actually, rq why is it "when canvas not available"? @t128n

Text is usually measured using createCanvasContext. On Vercel, createCanvasContext can’t be created (returns null, likely due to missing native node.js addons in the serveless runtime), so it falls back to estimateTextWidth. estimateTextWidth makes a best guess for characters’ widths and has edge cases with wider characters like w.
That's the whole context.

So Canvas not available was referring to the createCanvasContext (or what its name is - currently on mobile and it's hard to look outside the PRs boundary to find the imports).

@ghostdevv
Copy link
Copy Markdown
Contributor

ghostdevv commented Apr 14, 2026

actually, rq why is it "when canvas not available"? @t128n

Text is usually measured using createCanvasContext. On Vercel, createCanvasContext can’t be created (returns null, likely due to missing native node.js addons in the serveless runtime), so it falls back to estimateTextWidth. estimateTextWidth makes a best guess for characters’ widths and has edge cases with wider characters like w. That's the whole context.

So Canvas not available was referring to the createCanvasContext (or what its name is - currently on mobile and it's hard to look outside the PRs boundary to find the imports).

ah but that's always been true right? like that fact hasn't changed in this PR?

also that's why we have the napi-rs canvas? or no?

@t128n
Copy link
Copy Markdown
Contributor Author

t128n commented Apr 14, 2026

Yeah, my assumptions were flawed - I thought it would be about the whole canvas not being available, but I made some further tests real quick.

So, @napi-rs/canvas & canvasContext generally are available, but if I'm not missing anything major, it's producing 0 widths because Geist as a font is not available on the Vercel serverless function (my assumption based on the testing).

function measureTextWidth(text: string, font: string): number | null {
  const context = getCanvasContext()

  if (context) {
    context.font = font

    const measuredWidth = context.measureText(text).width

    if (Number.isFinite(measuredWidth) && measuredWidth > 0) { // Returns false as font not available and nothing can be measured
      return Math.ceil(measuredWidth)
    }
  }

  return null // yields null for canvas context and then falls back to estimateTextWidth 
}

So it's rather: use pre-measured widths for a lookup table rather than use table for badges when canvas not available

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs review This PR is waiting for a review from a maintainer ux Related to wider UX decisions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Measuring text for the purpose of rendering badges fails on Vercel

3 participants