Compare commits

...
Author SHA1 Message Date
netcatty-bot 6d9b78136f fix: address Codex review on PR #3336 2026-09-09 04:09:21 +00:00
netcatty-bot 72cb593e9a fix: address Codex review on PR #3336 2026-09-09 03:48:17 +00:00
netcatty-bot 253521c6aa fix(#3335): automated AI fix 2026-09-09 03:26:10 +00:00
2 changed files with 122 additions and 1 deletions

No files matched your search

+45
View File
@@ -64,6 +64,51 @@ describe('isBoldWeightDistinctWithContext', () => {
});
assert.equal(isBoldWeightDistinctWithContext('Menlo', 400, 700, 14, ctx), false);
});
it('detects metric-aligned variable-font bold via ink coverage (#3335)', () => {
// Roboto Mono VF: identical width/ascent/descent across wght, but real
// weights carry proportionally more ink (measured ~1.25x for 700/400).
const inkByWeight: Partial<Record<number, number>> = { 400: 1000, 700: 1252 };
const ctx = {
measureText: () =>
({
width: 117.6191,
actualBoundingBoxAscent: 10.0967,
actualBoundingBoxDescent: 0.1367,
}) as TextMetrics,
measureTextInk: (font: string) => {
const weight = Number(font.match(/^(\d+)\s/)?.[1] ?? 400);
return inkByWeight[weight] ?? 1000;
},
};
assert.equal(isBoldWeightDistinctWithContext('Roboto Mono', 400, 700, 14, ctx), true);
});
it('keeps rejecting a bold weight whose ink matches the normal weight', () => {
const ctx = {
measureText: () =>
({
width: 117.6191,
actualBoundingBoxAscent: 10.0967,
actualBoundingBoxDescent: 0.1367,
}) as TextMetrics,
measureTextInk: () => 1000,
};
assert.equal(isBoldWeightDistinctWithContext('Roboto Mono', 400, 700, 14, ctx), false);
});
it('ignores sub-threshold ink jitter between weights', () => {
const ctx = {
measureText: () =>
({
width: 117.6191,
actualBoundingBoxAscent: 10.0967,
actualBoundingBoxDescent: 0.1367,
}) as TextMetrics,
measureTextInk: (font: string) => (font.startsWith('700 ') ? 1005 : 1000),
};
assert.equal(isBoldWeightDistinctWithContext('Roboto Mono', 400, 700, 14, ctx), false);
});
});
describe('resolveFontWeightBold', () => {
+77 -1
View File
@@ -7,6 +7,13 @@ export const BUNDLED_FONT_WEIGHTS: Readonly<Record<string, readonly number[]>> =
export type FontWeightMeasureContext = {
measureText: (font: string, text: string) => TextMetrics;
/**
* Optional stroke-ink probe: total alpha coverage of `text` rendered with
* `font` on an offscreen canvas. Unlike geometric TextMetrics, ink coverage
* distinguishes real heavier weights even in metric-aligned monospace
* (variable) fonts where width/ascent/descent stay identical across `wght`.
*/
measureTextInk?: (font: string, text: string) => number;
};
const BOLD_PROBE = 'WMwm0123456789';
@@ -29,6 +36,10 @@ export function pickNearestBundledWeight(
* `normalWeight` for `family`. Unlike document.fonts.check(), this does not
* false-positive on syntactically valid but unavailable families/weights in
* Chromium (see fontAvailability.ts).
*
* Compares geometric TextMetrics first, then, for metric-aligned monospace
* variable fonts where every geometric delta is ~0 (#3335), actual stroke
* ink coverage via `measureTextInk`.
*/
export function isBoldWeightDistinctWithContext(
family: string,
@@ -54,7 +65,65 @@ export function isBoldWeightDistinctWithContext(
const normalDescent = normalMetrics.actualBoundingBoxDescent ?? 0;
const boldDescent = boldMetrics.actualBoundingBoxDescent ?? 0;
return Math.abs(boldDescent - normalDescent) > 0.01;
if (Math.abs(boldDescent - normalDescent) > 0.01) return true;
// Metric-aligned monospace fonts (especially variable fonts such as
// Roboto Mono VF) keep advance width, cap height and baseline contact
// constant across the wght axis by design, so the geometric probes above
// all read ~0 even though the bold instance carries ~25% more ink.
// Fall back to comparing actual stroke coverage, which is what font
// weight really is (#3335).
if (!ctx.measureTextInk) return false;
const normalInk = ctx.measureTextInk(normalFont, BOLD_PROBE);
const boldInk = ctx.measureTextInk(boldFont, BOLD_PROBE);
if (normalInk <= 0) return boldInk > 0;
return boldInk / normalInk >= 1 + BOLD_INK_RATIO_THRESHOLD;
}
/**
* Relative ink increase required to accept a bold weight once the geometric
* probes are inconclusive. Real heavier weights add far more than this
* (Roboto Mono 400 to 700 is about +25%); identical rendering differs by ~0%.
*/
const BOLD_INK_RATIO_THRESHOLD = 0.03;
let inkCanvas: HTMLCanvasElement | null = null;
let inkCtx: CanvasRenderingContext2D | null = null;
/**
* Sum of the alpha channel for `text` rasterized with `font`: a direct
* measure of stroke ink, so a heavier weight always reads higher even when
* every geometric metric is identical across the wght axis (#3335).
*/
function measureInkCoverage(ctx: CanvasRenderingContext2D, font: string, text: string): number {
const sizeMatch = font.match(/(\d+(?:\.\d+)?)px/);
const px = sizeMatch ? parseFloat(sizeMatch[1]) : 16;
// Measure with the target font so ascent/descent/width reflect the actual
// glyphs (the context's default font would undersize the canvas and clip).
ctx.font = font;
const metrics = ctx.measureText(text);
const ascent = Math.ceil(metrics.actualBoundingBoxAscent || px * 0.8) + 2;
const descent = Math.ceil(metrics.actualBoundingBoxDescent || px * 0.2) + 2;
const width = Math.ceil(Math.abs(metrics.width)) + 4;
if (width <= 4 || ascent + descent <= 4) return 0;
if (!inkCanvas) {
inkCanvas = document.createElement('canvas');
inkCtx = inkCanvas.getContext('2d', { willReadFrequently: true });
}
if (!inkCtx) return 0;
inkCanvas.width = width;
inkCanvas.height = ascent + descent;
inkCtx.clearRect(0, 0, width, ascent + descent);
inkCtx.font = font;
inkCtx.fillStyle = '#fff';
inkCtx.textBaseline = 'alphabetic';
inkCtx.fillText(text, 2, ascent);
const data = inkCtx.getImageData(0, 0, width, ascent + descent).data;
let ink = 0;
for (let i = 3; i < data.length; i += 4) ink += data[i];
return ink;
}
function buildBrowserMeasureContext(): FontWeightMeasureContext | null {
@@ -67,6 +136,13 @@ function buildBrowserMeasureContext(): FontWeightMeasureContext | null {
ctx.font = font;
return ctx.measureText(text);
},
measureTextInk: (font, text) => {
if (!inkCtx) {
inkCanvas = document.createElement('canvas');
inkCtx = inkCanvas.getContext('2d', { willReadFrequently: true });
}
return inkCtx ? measureInkCoverage(inkCtx, font, text) : 0;
},
};
}