Skip to content

Commit ad9d0a0

Browse files
authored
Merge pull request #1306 from openlayers/font-loading
Make font detection and loading independent from OpenLayers
2 parents 72a0fff + fa8632a commit ad9d0a0

File tree

6 files changed

+93
-87
lines changed

6 files changed

+93
-87
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
},
4343
"dependencies": {
4444
"@maplibre/maplibre-gl-style-spec": "^23.1.0",
45-
"mapbox-to-css-font": "^3.1.2"
45+
"mapbox-to-css-font": "^3.2.0"
4646
},
4747
"peerDependencies": {
4848
"ol": "*"

src/text.js

Lines changed: 33 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import mb2css from 'mapbox-to-css-font';
2-
import {checkedFonts, registerFont} from 'ol/render/canvas.js';
32
import {createCanvas} from './util.js';
43

54
const hairSpacePool = Array(256).join('\u200A');
@@ -119,33 +118,17 @@ export function wrapText(text, font, em, letterSpacing) {
119118
return wrappedText;
120119
}
121120

122-
const fontFamilyRegEx = /font-family: ?([^;]*);/;
123-
const stripQuotesRegEx = /("|')/g;
124-
let loadedFontFamilies;
125-
function hasFontFamily(family) {
126-
if (!loadedFontFamilies) {
127-
loadedFontFamilies = {};
128-
const styleSheets = document.styleSheets;
129-
for (let i = 0, ii = styleSheets.length; i < ii; ++i) {
130-
const styleSheet = /** @type {CSSStyleSheet} */ (styleSheets[i]);
131-
try {
132-
const cssRules = styleSheet.rules || styleSheet.cssRules;
133-
if (cssRules) {
134-
for (let j = 0, jj = cssRules.length; j < jj; ++j) {
135-
const cssRule = cssRules[j];
136-
if (cssRule.type == 5) {
137-
const match = cssRule.cssText.match(fontFamilyRegEx);
138-
loadedFontFamilies[match[1].replace(stripQuotesRegEx, '')] = true;
139-
}
140-
}
141-
}
142-
} catch {
143-
// empty catch block
144-
}
145-
}
146-
}
147-
return family in loadedFontFamilies;
148-
}
121+
const webSafeFonts = [
122+
'Arial',
123+
'Courier New',
124+
'Times New Roman',
125+
'Verdana',
126+
'sans-serif',
127+
'serif',
128+
'monospace',
129+
'cursive',
130+
'fantasy',
131+
];
149132

150133
const processedFontFamilies = {};
151134

@@ -159,41 +142,45 @@ export function getFonts(
159142
fonts,
160143
templateUrl = 'https://cdn.jsdelivr.net/npm/@fontsource/{font-family}/{fontweight}{-fontstyle}.css',
161144
) {
162-
const fontsKey = fonts.toString();
163-
if (fontsKey in processedFontFamilies) {
164-
return processedFontFamilies[fontsKey];
165-
}
166145
const fontDescriptions = [];
167146
for (let i = 0, ii = fonts.length; i < ii; ++i) {
168-
fonts[i] = fonts[i].replace('Arial Unicode MS', 'Arial');
169147
const font = fonts[i];
148+
if (font in processedFontFamilies) {
149+
continue;
150+
}
151+
processedFontFamilies[font] = true;
170152
const cssFont = mb2css(font, 1);
171-
registerFont(cssFont);
172153
const parts = cssFont.split(' ');
173154
fontDescriptions.push([
174155
parts.slice(3).join(' ').replace(/"/g, ''),
175156
parts[1],
176157
parts[0],
177158
]);
178159
}
179-
for (let i = 0, ii = fontDescriptions.length; i < ii; ++i) {
180-
const fontDescription = fontDescriptions[i];
181-
const family = fontDescription[0];
182-
if (!hasFontFamily(family)) {
160+
161+
(async () => {
162+
await document.fonts.ready;
163+
for (let i = 0, ii = fontDescriptions.length; i < ii; ++i) {
164+
const fontDescription = fontDescriptions[i];
165+
const family = fontDescription[0];
166+
if (webSafeFonts.includes(family)) {
167+
continue;
168+
}
169+
const weight = fontDescription[1];
170+
const style = fontDescription[2];
183171
if (
184-
checkedFonts.get(
185-
`${fontDescription[2]}\n${fontDescription[1]} \n${family}`,
186-
) !== 100
172+
(await document.fonts.load(`${style} ${weight} 16px "${family}"`))
173+
.length === 0
187174
) {
188175
const fontUrl = templateUrl
189176
.replace('{font-family}', family.replace(/ /g, '-').toLowerCase())
190177
.replace('{Font+Family}', family.replace(/ /g, '+'))
191-
.replace('{fontweight}', fontDescription[1])
178+
.replace('{fontweight}', weight)
192179
.replace(
193180
'{-fontstyle}',
194-
fontDescription[2].replace('normal', '').replace(/(.+)/, '-$1'),
181+
style.replace('normal', '').replace(/(.+)/, '-$1'),
195182
)
196-
.replace('{fontstyle}', fontDescription[2]);
183+
.replace('{fontstyle}', style);
197184
if (!document.querySelector('link[href="' + fontUrl + '"]')) {
198185
const markup = document.createElement('link');
199186
markup.href = fontUrl;
@@ -202,7 +189,7 @@ export function getFonts(
202189
}
203190
}
204191
}
205-
}
206-
processedFontFamilies[fontsKey] = fonts;
192+
})();
193+
207194
return fonts;
208195
}

test/apply.test.js

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import {defaultResolutions, getZoomForResolution} from '../src/util.js';
2525
import backgroundNoneStyle from './fixtures/background-none.json';
2626
import backgroundStyle from './fixtures/background.json';
27+
import {createObserver} from './util.test.js';
2728
delete brightV9.sprite;
2829

2930
describe('ol-mapbox-style', function () {
@@ -1142,8 +1143,8 @@ describe('ol-mapbox-style', function () {
11421143
});
11431144
});
11441145

1145-
it('loads fonts from a style', function (done) {
1146-
apply(target, {
1146+
it('loads fonts from a style', async function () {
1147+
const map = await apply(target, {
11471148
version: 8,
11481149
metadata: {
11491150
'ol:webfonts':
@@ -1169,20 +1170,15 @@ describe('ol-mapbox-style', function () {
11691170
},
11701171
},
11711172
],
1172-
})
1173-
.then(function (map) {
1174-
const getStyle = map.getAllLayers()[0].getStyle();
1175-
getStyle(new Feature(new Point([0, 0])), 1);
1176-
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
1177-
should(stylesheets.length).eql(1);
1178-
should(stylesheets.item(0).href).eql(
1179-
'https://fonts.openmaptiles.org/open-sans/400.css',
1180-
);
1181-
done();
1182-
})
1183-
.catch(function (err) {
1184-
done(err);
1185-
});
1173+
});
1174+
const getStyle = map.getAllLayers()[0].getStyle();
1175+
getStyle(new Feature(new Point([0, 0])), 1);
1176+
await createObserver(1);
1177+
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
1178+
should(stylesheets.length).eql(1);
1179+
should(stylesheets.item(0).href).eql(
1180+
'https://fonts.openmaptiles.org/open-sans/400.css',
1181+
);
11861182
});
11871183

11881184
it('loads fonts from the webfonts option', function (done) {
@@ -1216,9 +1212,10 @@ describe('ol-mapbox-style', function () {
12161212
'https://fonts.openmaptiles.org/{font-family}/{fontweight}{-fontstyle}.css',
12171213
},
12181214
)
1219-
.then(function (map) {
1215+
.then(async function (map) {
12201216
const getStyle = map.getAllLayers()[0].getStyle();
12211217
getStyle(new Feature(new Point([0, 0])), 1);
1218+
await createObserver(1);
12221219
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
12231220
should(stylesheets.length).eql(1);
12241221
should(stylesheets.item(0).href).eql(

test/text.test.js

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import should from 'should';
22
import {getFonts, wrapText} from '../src/text.js';
3+
import {createObserver} from './util.test.js';
34

45
describe('text', function () {
56
describe('wrapText()', function () {
@@ -41,11 +42,12 @@ describe('text', function () {
4142
});
4243

4344
describe('getFonts', function () {
44-
before(function () {
45+
beforeEach(function () {
4546
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
4647
stylesheets.forEach(function (stylesheet) {
4748
stylesheet.remove();
4849
});
50+
document.fonts.clear();
4951
});
5052

5153
it('does not load standard fonts', function () {
@@ -54,7 +56,7 @@ describe('text', function () {
5456
should(stylesheets.length).eql(0);
5557
});
5658

57-
it('loads fonts with a template using {Font+Family} and {fontstyle}', function () {
59+
it('loads fonts with a template using {Font+Family} and {fontstyle}', async function () {
5860
getFonts(
5961
[
6062
'Noto Sans Bold',
@@ -63,7 +65,8 @@ describe('text', function () {
6365
],
6466
'https://fonts.googleapis.com/css?family={Font+Family}:{fontweight}{fontstyle}',
6567
);
66-
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
68+
await createObserver(3);
69+
let stylesheets = document.querySelectorAll('link[rel=stylesheet]');
6770
should(stylesheets.length).eql(3);
6871
should(stylesheets.item(0).href).eql(
6972
'https://fonts.googleapis.com/css?family=Noto+Sans:700normal',
@@ -74,37 +77,39 @@ describe('text', function () {
7477
should(stylesheets.item(2).href).eql(
7578
'https://fonts.googleapis.com/css?family=Averia+Sans+Libre:700normal',
7679
);
80+
81+
// Does not load the same font twice
82+
getFonts(
83+
['Noto Sans Bold'],
84+
'https://fonts.googleapis.com/css?family={Font+Family}:{fontweight}{fontstyle}',
85+
);
86+
await new Promise((resolve) => setTimeout(resolve, 500));
87+
stylesheets = document.querySelectorAll('link[rel=stylesheet]');
88+
should(stylesheets.length).eql(3);
7789
});
7890

79-
it('loads fonts with a template using {font-family} and {-fontstyle}', function () {
91+
it('loads fonts with a template using {font-family} and {-fontstyle}', async function () {
8092
getFonts(
8193
['Noto Sans Regular', 'Averia Sans Libre Bold Italic'],
8294
'./fonts/{font-family}/{fontweight}{-fontstyle}.css',
8395
);
96+
await createObserver(2);
8497
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
85-
should(stylesheets.length).eql(5);
86-
should(stylesheets.item(3).href).eql(
98+
should(stylesheets.length).eql(2);
99+
should(stylesheets.item(0).href).eql(
87100
location.origin + '/fonts/noto-sans/400.css',
88101
);
89-
should(stylesheets.item(4).href).eql(
102+
should(stylesheets.item(1).href).eql(
90103
location.origin + '/fonts/averia-sans-libre/700-italic.css',
91104
);
92105
});
93106

94-
it('does not load fonts twice', function () {
95-
getFonts(
96-
['Noto Sans Bold'],
97-
'https://fonts.googleapis.com/css?family={Font+Family}:{fontweight}{fontstyle}',
98-
);
99-
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
100-
should(stylesheets.length).eql(5);
101-
});
102-
103-
it('uses the default template if none is provided', function () {
107+
it('uses the default template if none is provided', async function () {
104108
getFonts(['Averia Sans Libre']);
109+
await createObserver(1);
105110
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
106-
should(stylesheets.length).eql(6);
107-
should(stylesheets.item(5).href).eql(
111+
should(stylesheets.length).eql(1);
112+
should(stylesheets.item(0).href).eql(
108113
'https://cdn.jsdelivr.net/npm/@fontsource/averia-sans-libre/400.css',
109114
);
110115
});

test/util.test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ import {
2020
} from '../src/apply.js';
2121
import {fetchResource} from '../src/util.js';
2222

23+
export function createObserver(total) {
24+
return new Promise((resolve) => {
25+
let count = 0;
26+
const observer = new MutationObserver(() => {
27+
count++;
28+
if (count === total) {
29+
observer.disconnect();
30+
resolve();
31+
}
32+
});
33+
observer.observe(document.head, {
34+
childList: true,
35+
subtree: true,
36+
});
37+
});
38+
}
39+
2340
describe('util', function () {
2441
describe('fetchResource', function () {
2542
it('allows to transform requests with the transformRequest option', function (done) {

0 commit comments

Comments
 (0)