Skip to content

Commit 5a1a633

Browse files
authored
refactor(measure-text): simplify browser check and improve text measurement logic (#203)
* refactor(measure-text): simplify browser check and improve text measurement logic * chore: update version * refactor: optimize get canvas context
1 parent d03fde1 commit 5a1a633

4 files changed

Lines changed: 127 additions & 8 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
3+
const fallbackMeasureText = vi.fn(() => ({ width: 9, height: 11 }));
4+
5+
vi.mock('measury', () => ({
6+
measureText: fallbackMeasureText,
7+
registerFont: vi.fn(),
8+
}));
9+
10+
vi.mock('measury/fonts/AlibabaPuHuiTi-Regular', () => ({
11+
default: {
12+
fontFamily: 'AlibabaPuHuiTi-Regular',
13+
},
14+
}));
15+
16+
describe('measureText', () => {
17+
afterEach(() => {
18+
vi.restoreAllMocks();
19+
vi.resetModules();
20+
fallbackMeasureText.mockClear();
21+
});
22+
23+
it('prefers canvas metrics when canvas is available even if layout is unavailable', async () => {
24+
const originalCreateElement = document.createElement.bind(document);
25+
vi.spyOn(document, 'createElement').mockImplementation(
26+
(tagName: string, options?: ElementCreationOptions) => {
27+
const element = originalCreateElement(
28+
tagName as keyof HTMLElementTagNameMap,
29+
options,
30+
);
31+
if (tagName === 'span' || tagName === 'div') {
32+
vi.spyOn(element, 'getBoundingClientRect').mockReturnValue({
33+
width: 0,
34+
height: 0,
35+
left: 0,
36+
top: 0,
37+
right: 0,
38+
bottom: 0,
39+
x: 0,
40+
y: 0,
41+
toJSON: () => ({}),
42+
} as DOMRect);
43+
}
44+
return element;
45+
},
46+
);
47+
48+
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
49+
font: '',
50+
measureText: () => ({ width: 120 }),
51+
} as unknown as CanvasRenderingContext2D);
52+
53+
const { measureText } = await import('../../../src/utils/measure-text');
54+
const metrics = measureText('数字化转型层级', {
55+
fontFamily: "'Times New Roman', Times, serif, SimSun, STSong",
56+
fontSize: 20,
57+
fontWeight: 'normal',
58+
});
59+
60+
expect(metrics.width).toBe(122);
61+
expect(metrics.height).toBe(29);
62+
expect(fallbackMeasureText).not.toHaveBeenCalled();
63+
});
64+
65+
it('falls back to measury when canvas is unavailable and span layout is invalid', async () => {
66+
const originalCreateElement = document.createElement.bind(document);
67+
vi.spyOn(document, 'createElement').mockImplementation(
68+
(tagName: string, options?: ElementCreationOptions) => {
69+
const element = originalCreateElement(
70+
tagName as keyof HTMLElementTagNameMap,
71+
options,
72+
);
73+
if (tagName === 'span') {
74+
vi.spyOn(element, 'getBoundingClientRect').mockReturnValue({
75+
width: 0,
76+
height: 0,
77+
left: 0,
78+
top: 0,
79+
right: 0,
80+
bottom: 0,
81+
x: 0,
82+
y: 0,
83+
toJSON: () => ({}),
84+
} as DOMRect);
85+
}
86+
return element;
87+
},
88+
);
89+
90+
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null);
91+
92+
const { measureText } = await import('../../../src/utils/measure-text');
93+
const metrics = measureText('数字化转型层级', {
94+
fontFamily: "'Times New Roman', Times, serif, SimSun, STSong",
95+
fontSize: 20,
96+
fontWeight: 'normal',
97+
});
98+
99+
expect(metrics.width).toBe(10);
100+
expect(metrics.height).toBe(12);
101+
expect(fallbackMeasureText).toHaveBeenCalledTimes(1);
102+
});
103+
104+
it('falls back to measury when document is unavailable', async () => {
105+
const { measureText } = await import('../../../src/utils/measure-text');
106+
vi.stubGlobal('window', undefined);
107+
vi.stubGlobal('document', undefined);
108+
109+
const metrics = measureText('数字化转型层级', {
110+
fontFamily: "'Times New Roman', Times, serif, SimSun, STSong",
111+
fontSize: 20,
112+
fontWeight: 'normal',
113+
});
114+
115+
expect(metrics.width).toBe(10);
116+
expect(metrics.height).toBe(12);
117+
expect(fallbackMeasureText).toHaveBeenCalledTimes(1);
118+
});
119+
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@antv/infographic",
3-
"version": "0.2.13",
3+
"version": "0.2.14",
44
"description": "An Infographic Generation and Rendering Framework, bring words to life!",
55
"keywords": [
66
"antv",

src/utils/measure-text.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import AlibabaPuHuiTi from 'measury/fonts/AlibabaPuHuiTi-Regular';
33
import { JSXNode, TextProps } from '../jsx';
44
import { DEFAULT_FONT } from '../renderer';
55
import { encodeFontFamily } from './font';
6-
import { isBrowser } from './is-browser';
76

87
let FONT_EXTEND_FACTOR = 1.01;
98

@@ -13,17 +12,19 @@ export const setFontExtendFactor = (factor: number) => {
1312

1413
registerFont(AlibabaPuHuiTi);
1514

16-
let canvasContext: CanvasRenderingContext2D | null = null;
15+
let canvasContext: CanvasRenderingContext2D | null | undefined = undefined;
1716
let measureSpan: HTMLSpanElement | null = null;
1817

1918
function getCanvasContext() {
20-
if (canvasContext) return canvasContext;
19+
if (typeof document === 'undefined') return null;
20+
if (canvasContext !== undefined) return canvasContext;
2121
const canvas = document.createElement('canvas');
2222
canvasContext = canvas.getContext('2d');
2323
return canvasContext;
2424
}
2525

2626
function getMeasureSpan() {
27+
if (typeof document === 'undefined') return null;
2728
if (!document.body) return null;
2829
if (measureSpan) return measureSpan;
2930
measureSpan = document.createElement('span');
@@ -98,6 +99,7 @@ function measureTextInBrowser(
9899
span.style.lineHeight = `${lineHeightPx}px`;
99100
span.textContent = content;
100101
const rect = span.getBoundingClientRect();
102+
if (content && rect.width <= 0 && rect.height <= 0) return null;
101103
return { width: rect.width, height: rect.height };
102104
}
103105

@@ -126,9 +128,7 @@ export function measureText(
126128
lineHeight,
127129
};
128130
const fallback = () => measure(content, options);
129-
const metrics = isBrowser()
130-
? (measureTextInBrowser(content, options) ?? fallback())
131-
: fallback();
131+
const metrics = measureTextInBrowser(content, options) ?? fallback();
132132

133133
// 额外添加 1% 宽高
134134
return {

src/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const VERSION = '0.2.13';
1+
export const VERSION = '0.2.14';

0 commit comments

Comments
 (0)