Skip to content

feat: improve badge width estimation#2487

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

feat: improve badge width estimation#2487
t128n wants to merge 3 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)
docs.npmx.dev Ready Ready Preview, Comment Apr 19, 2026 10:30pm
npmx.dev Ready Ready Preview, Comment Apr 19, 2026 10:30pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
npmx-lunaria Ignored Ignored Apr 19, 2026 10:30pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

Replaced heuristic, bucket-based text width estimation with explicit per-character width lookup tables for supported badge fonts and a fixed fallback width for unmapped characters; updated estimateTextWidth to use font and sum per-character widths.

Changes

Cohort / File(s) Summary
Badge Text-Width Measurement
server/api/registry/badge/[type]/[...pkg].get.ts
Removed regex/set-based categorisation and FALLBACK_WIDTHS/NARROW_CHARS/MEDIUM_CHARS buckets. Added CHAR_WIDTHS for default and shieldsio fonts, CHAR_WIDTH_FALLBACK for unknown characters, and refactored estimateTextWidth(font, text) to sum per-character widths with fallback.

Possibly related PRs

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description is comprehensive and clearly related to the changeset, detailing the problem (Canvas measurement failures on Vercel), the solution (per-character lookup table), and providing extensive visual comparisons demonstrating the fix.
Linked Issues check ✅ Passed The PR successfully addresses issue #1601 by implementing a per-character lookup table for accurate text-width measurement when Canvas-based measurement fails on Vercel, preventing badge overflow and ensuring consistent rendering across environments.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing text-width estimation for badges by replacing the heuristic approach with a lookup table; no unrelated modifications or refactoring are present.
Title check ✅ Passed The title 'feat: improve badge width estimation' is directly related to the main change: replacing a heuristic text width estimator with a deterministic per-character lookup table to improve badge width estimation accuracy.

✏️ 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

@wojtekmaj
Copy link
Copy Markdown
Contributor

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

Yea, it is my flawed implementation that I wasn't able to debug because I didn't know what errors were happening on Vercel, and it is not reproducible locally at all.

@ghostdevv
Copy link
Copy Markdown
Contributor

I think we can create an issue to try and load the font and see what the perf is like on that, if we can get it working that is. If there are Vercel issues then we can help with debugging that this time around as we should now have access to logs

@ghostdevv ghostdevv changed the title fix: use lookup table for badges char widths when canvas not available fix: improve badge width estimation Apr 19, 2026
@ghostdevv ghostdevv changed the title fix: improve badge width estimation feat: improve badge width estimation Apr 19, 2026
@ghostdevv ghostdevv enabled auto-merge April 19, 2026 22:34
@ghostdevv ghostdevv removed the needs review This PR is waiting for a review from a maintainer label Apr 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

4 participants