fingerprintjs
151 строка · 4.5 Кб
1import { releaseEventLoop } from '../utils/async'
2import { withIframe } from '../utils/dom'
3
4// We use m or w because these two characters take up the maximum width.
5// And we use a LLi so that the same matching fonts can get separated.
6const testString = 'mmMwWLliI0O&1'
7
8// We test using 48px font size, we may use any size. I guess larger the better.
9const textSize = '48px'
10
11// A font will be compared against all the three default fonts.
12// And if for any default fonts it doesn't match, then that font is available.
13const baseFonts = ['monospace', 'sans-serif', 'serif'] as const
14
15const fontList = [
16// This is android-specific font from "Roboto" family
17'sans-serif-thin',
18'ARNO PRO',
19'Agency FB',
20'Arabic Typesetting',
21'Arial Unicode MS',
22'AvantGarde Bk BT',
23'BankGothic Md BT',
24'Batang',
25'Bitstream Vera Sans Mono',
26'Calibri',
27'Century',
28'Century Gothic',
29'Clarendon',
30'EUROSTILE',
31'Franklin Gothic',
32'Futura Bk BT',
33'Futura Md BT',
34'GOTHAM',
35'Gill Sans',
36'HELV',
37'Haettenschweiler',
38'Helvetica Neue',
39'Humanst521 BT',
40'Leelawadee',
41'Letter Gothic',
42'Levenim MT',
43'Lucida Bright',
44'Lucida Sans',
45'Menlo',
46'MS Mincho',
47'MS Outlook',
48'MS Reference Specialty',
49'MS UI Gothic',
50'MT Extra',
51'MYRIAD PRO',
52'Marlett',
53'Meiryo UI',
54'Microsoft Uighur',
55'Minion Pro',
56'Monotype Corsiva',
57'PMingLiU',
58'Pristina',
59'SCRIPTINA',
60'Segoe UI Light',
61'Serifa',
62'SimHei',
63'Small Fonts',
64'Staccato222 BT',
65'TRAJAN PRO',
66'Univers CE 55 Medium',
67'Vrinda',
68'ZWAdobeF',
69] as const
70
71// kudos to http://www.lalit.org/lab/javascript-css-font-detect/
72export default function getFonts(): Promise<string[]> {
73// Running the script in an iframe makes it not affect the page look and not be affected by the page CSS. See:
74// https://github.com/fingerprintjs/fingerprintjs/issues/592
75// https://github.com/fingerprintjs/fingerprintjs/issues/628
76return withIframe(async (_, { document }) => {
77const holder = document.body
78holder.style.fontSize = textSize
79
80// div to load spans for the default fonts and the fonts to detect
81const spansContainer = document.createElement('div')
82spansContainer.style.setProperty('visibility', 'hidden', 'important')
83
84const defaultWidth: Partial<Record<string, number>> = {}
85const defaultHeight: Partial<Record<string, number>> = {}
86
87// creates a span where the fonts will be loaded
88const createSpan = (fontFamily: string) => {
89const span = document.createElement('span')
90const { style } = span
91style.position = 'absolute'
92style.top = '0'
93style.left = '0'
94style.fontFamily = fontFamily
95span.textContent = testString
96spansContainer.appendChild(span)
97return span
98}
99
100// creates a span and load the font to detect and a base font for fallback
101const createSpanWithFonts = (fontToDetect: string, baseFont: string) => {
102return createSpan(`'${fontToDetect}',${baseFont}`)
103}
104
105// creates spans for the base fonts and adds them to baseFontsDiv
106const initializeBaseFontsSpans = () => {
107return baseFonts.map(createSpan)
108}
109
110// creates spans for the fonts to detect and adds them to fontsDiv
111const initializeFontsSpans = () => {
112// Stores {fontName : [spans for that font]}
113const spans: Record<string, HTMLSpanElement[]> = {}
114
115for (const font of fontList) {
116spans[font] = baseFonts.map((baseFont) => createSpanWithFonts(font, baseFont))
117}
118
119return spans
120}
121
122// checks if a font is available
123const isFontAvailable = (fontSpans: HTMLElement[]) => {
124return baseFonts.some(
125(baseFont, baseFontIndex) =>
126fontSpans[baseFontIndex].offsetWidth !== defaultWidth[baseFont] ||
127fontSpans[baseFontIndex].offsetHeight !== defaultHeight[baseFont],
128)
129}
130
131// create spans for base fonts
132const baseFontsSpans = initializeBaseFontsSpans()
133
134// create spans for fonts to detect
135const fontsSpans = initializeFontsSpans()
136
137// add all the spans to the DOM
138holder.appendChild(spansContainer)
139
140await releaseEventLoop()
141
142// get the default width for the three base fonts
143for (let index = 0; index < baseFonts.length; index++) {
144defaultWidth[baseFonts[index]] = baseFontsSpans[index].offsetWidth // width for the default font
145defaultHeight[baseFonts[index]] = baseFontsSpans[index].offsetHeight // height for the default font
146}
147
148// check available fonts
149return fontList.filter((font) => isFontAvailable(fontsSpans[font]))
150})
151}
152