Skip to content

Commit

Permalink
Merge pull request #851 from openlayers/fonts
Browse files Browse the repository at this point in the history
Flexible font handling without Google Fonts default
  • Loading branch information
ahocevar committed Apr 5, 2023
2 parents 0ca2932 + a340702 commit 0b6b743
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 30 deletions.
23 changes: 22 additions & 1 deletion README.md
Expand Up @@ -70,7 +70,28 @@ Note that this low-level API does not create a source for the layer, and extra w

### Font handling

Only commonly available system fonts and [Google Fonts](https://developers.google.com/fonts/) will automatically be available for any `text-font` defined in the Mapbox Style object. It is the responsibility of the application to load other fonts. Because `ol-mapbox-style` uses system and web fonts instead of PBF/SDF glyphs, the [font stack](https://www.mapbox.com/help/manage-fontstacks/) is treated a little different: style and weight are taken from the primary font (i.e. the first one in the font stack). Subsequent fonts in the font stack are only used if the primary font is not available/loaded, and they will be used with the style and weight of the primary font.
`ol-mapbox-style` cannot use PBF/SDF glyphs for `text-font` layout property, as defined in the Mapbox Style specification. Instead, it relies on web fonts. A `ol-webfonts` metadata property can be set on the root of the Style object to specify a location for webfonts, e.g.
```js
{
"version": 8,
"metadata": {
"ol-webfonts": "https://my.server/fonts/{font-family}/{fontweight}{-fontstyle}.css"
}
// ...
}
```

The following placeholders can be used in the `ol-webfonts` url:

* `{font-family}`: CSS font family converted to lowercase, blanks replaced with -, e.g. noto-sans
* `{Font+Family}`: CSS font family in original case, blanks replaced with +, e.g. Noto+Sans
* `{fontweight}`: CSS font weight (numeric), e.g. 400, 700
* `{fontstyle}`: CSS font style, e.g. normal, italic
* `{-fontstyle}`: CSS font style other than normal, e.g. -italic or empty string for normal

If no `metadata['ol-webfonts']` property is available on the Style object, [Fontsource Fonts](https://fontsource.org/fonts) will be used. It is also possible for the application to load other fonts. If a font is already available in the browser, `ol-mapbox-style` will not load it.

Because of this difference, the [font stack](https://www.mapbox.com/help/manage-fontstacks/) is treated a little different than defined in the spec: style and weight are taken from the primary font (i.e. the first one in the font stack). Subsequent fonts in the font stack are only used if the primary font is not available/loaded, and they will be used with the style and weight of the primary font.

## Building the library

Expand Down
4 changes: 4 additions & 0 deletions examples/maptiler-hillshading.js
Expand Up @@ -19,6 +19,10 @@ fetch(`https://api.maptiler.com/maps/outdoor-v2/style.json?key=${key}`)
Object.assign({}, style, {
center: [13.783578, 47.609499],
zoom: 11,
metadata: Object.assign(style.metadata, {
'ol:webfonts':
'https://fonts.googleapis.com/css?family={Font+Family}:{fontweight}{fontstyle}',
}),
})
);
});
12 changes: 9 additions & 3 deletions src/stylefunction.js
Expand Up @@ -320,8 +320,9 @@ export const styleFunctionArgs = {};
* @param {string} spriteImageUrl Sprite image url for the sprite
* specified in the Mapbox Style object's `sprite` property. Only required if a
* `sprite` property is specified in the Mapbox Style object.
* @param {function(Array<string>):Array<string>} getFonts Function that
* receives a font stack as arguments, and returns a (modified) font stack that
* @param {function(Array<string>, string=):Array<string>} getFonts Function that
* receives a font stack and the url template from the GL style's `metadata['ol:webfonts']`
* property (if set) as arguments, and returns a (modified) font stack that
* is available. Font names are the names used in the Mapbox Style object. If
* not provided, the font stack will be used as-is. This function can also be
* used for loading web fonts.
Expand Down Expand Up @@ -1145,7 +1146,12 @@ export function stylefunction(
featureState
);
font = mb2css(
getFonts ? getFonts(fontArray) : fontArray,
getFonts
? getFonts(
fontArray,
glStyle.metadata ? glStyle.metadata['ol:webfonts'] : undefined
)
: fontArray,
textSize,
textLineHeight
);
Expand Down
33 changes: 20 additions & 13 deletions src/text.js
Expand Up @@ -151,42 +151,49 @@ const processedFontFamilies = {};

/**
* @param {Array} fonts Fonts.
* @param {string} [templateUrl] Template URL.
* @return {Array} Processed fonts.
* @private
*/
export function getFonts(fonts) {
export function getFonts(
fonts,
templateUrl = 'https://cdn.jsdelivr.net/npm/@fontsource/{font-family}/{fontweight}{-fontstyle}.css'
) {
const fontsKey = fonts.toString();
if (fontsKey in processedFontFamilies) {
return processedFontFamilies[fontsKey];
}
const googleFontDescriptions = [];
const fontDescriptions = [];
for (let i = 0, ii = fonts.length; i < ii; ++i) {
fonts[i] = fonts[i].replace('Arial Unicode MS', 'Arial');
const font = fonts[i];
const cssFont = mb2css(font, 1);
registerFont(cssFont);
const parts = cssFont.split(' ');
googleFontDescriptions.push([
fontDescriptions.push([
parts.slice(3).join(' ').replace(/"/g, ''),
parts[1],
parts[0],
]);
}
for (let i = 0, ii = googleFontDescriptions.length; i < ii; ++i) {
const googleFontDescription = googleFontDescriptions[i];
const family = googleFontDescription[0];
for (let i = 0, ii = fontDescriptions.length; i < ii; ++i) {
const fontDescription = fontDescriptions[i];
const family = fontDescription[0];
if (!hasFontFamily(family)) {
if (
checkedFonts.get(
`${googleFontDescription[2]}\n${googleFontDescription[1]} \n${family}`
`${fontDescription[2]}\n${fontDescription[1]} \n${family}`
) !== 100
) {
const fontUrl =
'https://fonts.googleapis.com/css?family=' +
family.replace(/ /g, '+') +
':' +
googleFontDescription[1] +
googleFontDescription[2];
const fontUrl = templateUrl
.replace('{font-family}', family.replace(/ /g, '-').toLowerCase())
.replace('{Font+Family}', family.replace(/ /g, '+'))
.replace('{fontweight}', fontDescription[1])
.replace(
'{-fontstyle}',
fontDescription[2].replace('normal', '').replace(/(.+)/, '-$1')
)
.replace('{fontstyle}', fontDescription[2]);
if (!document.querySelector('link[href="' + fontUrl + '"]')) {
const markup = document.createElement('link');
markup.href = fontUrl;
Expand Down
54 changes: 54 additions & 0 deletions test/apply.test.js
Expand Up @@ -1040,4 +1040,58 @@ describe('ol-mapbox-style', function () {
.catch(done);
});
});

describe('Font loading', function () {
let target;
beforeEach(function () {
target = document.createElement('div');
});

it('loads fonts from a style', function (done) {
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
stylesheets.forEach(function (stylesheet) {
stylesheet.remove();
});
apply(target, {
version: 8,
metadata: {
'ol:webfonts':
'https://fonts.openmaptiles.org/{font-family}/{fontweight}{-fontstyle}.css',
},
sources: {
test: {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [],
},
},
},
layers: [
{
id: 'test',
type: 'symbol',
source: 'test',
layout: {
'text-field': 'test',
'text-font': ['Open Sans Regular'],
},
},
],
})
.then(function (map) {
const getStyle = map.getAllLayers()[0].getStyle();
getStyle(new Feature(new Point([0, 0])), 1);
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
should(stylesheets.length).eql(1);
should(stylesheets.item(0).href).eql(
'https://fonts.openmaptiles.org/open-sans/400.css'
);
done();
})
.catch(function (err) {
done(err);
});
});
});
});
56 changes: 43 additions & 13 deletions test/text.test.js
Expand Up @@ -41,7 +41,7 @@ describe('text', function () {
});

describe('getFonts', function () {
beforeEach(function () {
before(function () {
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
stylesheets.forEach(function (stylesheet) {
stylesheet.remove();
Expand All @@ -54,14 +54,16 @@ describe('text', function () {
should(stylesheets.length).eql(0);
});

it('loads fonts from fonts.google.com', function () {
let stylesheets;
getFonts([
'Noto Sans Bold',
'Noto Sans Regular Italic',
'Averia Sans Libre Bold',
]);
stylesheets = document.querySelectorAll('link[rel=stylesheet]');
it('loads fonts with a template using {Font+Family} and {fontstyle}', function () {
getFonts(
[
'Noto Sans Bold',
'Noto Sans Regular Italic',
'Averia Sans Libre Bold',
],
'https://fonts.googleapis.com/css?family={Font+Family}:{fontweight}{fontstyle}'
);
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
should(stylesheets.length).eql(3);
should(stylesheets.item(0).href).eql(
'https://fonts.googleapis.com/css?family=Noto+Sans:700normal'
Expand All @@ -72,11 +74,39 @@ describe('text', function () {
should(stylesheets.item(2).href).eql(
'https://fonts.googleapis.com/css?family=Averia+Sans+Libre:700normal'
);
});

// already loaded family, no additional link
getFonts(['Noto Sans Bold']);
stylesheets = document.querySelectorAll('link[rel=stylesheet]');
should(stylesheets.length).eql(3);
it('loads fonts with a template using {font-family} and {-fontstyle}', function () {
getFonts(
['Noto Sans Regular', 'Averia Sans Libre Bold Italic'],
'./fonts/{font-family}/{fontweight}{-fontstyle}.css'
);
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
should(stylesheets.length).eql(5);
should(stylesheets.item(3).href).eql(
location.origin + '/fonts/noto-sans/400.css'
);
should(stylesheets.item(4).href).eql(
location.origin + '/fonts/averia-sans-libre/700-italic.css'
);
});

it('does not load fonts twice', function () {
getFonts(
['Noto Sans Bold'],
'https://fonts.googleapis.com/css?family={Font+Family}:{fontweight}{fontstyle}'
);
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
should(stylesheets.length).eql(5);
});

it('uses the default template if none is provided', function () {
getFonts(['Averia Sans Libre']);
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
should(stylesheets.length).eql(6);
should(stylesheets.item(5).href).eql(
'https://cdn.jsdelivr.net/npm/@fontsource/averia-sans-libre/400.css'
);
});
});
});

0 comments on commit 0b6b743

Please sign in to comment.