mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-08 00:50:06 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d9b78136f | ||
|
|
72cb593e9a | ||
|
|
253521c6aa |
No files matched your search
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user