Используйте расширенную типографику с локальными шрифтами
Узнайте, как Local Font Access API позволяет получить доступ к локально установленным шрифтам пользователя и получить о них низкоуровневую информацию.
Веб-безопасные шрифты
Если вы достаточно долго занимались веб-разработкой, возможно, вы помните так называемые веб-безопасные шрифты. Известно, что эти шрифты доступны практически во всех экземплярах наиболее часто используемых операционных систем (а именно Windows, macOS, наиболее распространенных дистрибутивах Linux, Android и iOS). В начале 2000-х годов Microsoft даже выступила c инициативой под названием «Основные шрифты TrueType для Интернета», которая предоставляла эти шрифты для бесплатной загрузки с целью «всякий раз, когда вы посещаете веб-сайт, на котором они указаны, вы будете видеть страницы именно так, как задумал дизайнер сайта". Да, это включает сайты, установленные в Comic Sans MS. Вот классический веб-безопасный стек шрифтов может выглядеть так:
body {
font-family: Helvetica, Arial, sans-serif;
}
Веб-шрифты
Дни, когда веб-шрифты действительно имели значение, давно прошли. Сегодня у нас есть веб-шрифты, некоторые из которых даже являются вариативными шрифтами, которые мы можем настроить, изменив значения для различных открытых осей. Вы можете использовать веб-шрифты, объявив блок @font-face
в начале CSS, в котором указываются файлы шрифтов для загрузки:
@font-face {
font-family: "FlamboyantSansSerif";
src: url("flamboyant.woff2");
}
После этого вы можете использовать пользовательский веб-шрифт, указав font-family
, как обычно:
body {
font-family: "FlamboyantSansSerif";
}
Локальные шрифты как fingerprint вектор
Большинство веб-шрифтов поступают из Интернета. Однако интересным фактом является то, что свойство src
в объявлении @font-face
, помимо функции url()
, также принимает функцию local()
. Это позволяет загружать пользовательские шрифты (сюрприз!) Локально. Если в операционной системе пользователя установлен FlamboyantSansSerif, вместо загрузки будет использована локальная копия:
@font-face {
font-family: "FlamboyantSansSerif";
src: local("FlamboyantSansSerif"),
url("flamboyant.woff2");
}
Этот подход обеспечивает хороший запасной механизм, который потенциально экономит пропускную способность. В Интернете, к сожалению, не может быть хороших вещей. Проблема с этой функцией local()
в том, что она может использоваться для снятия fingerprint браузера. Оказывается, список шрифтов, установленных пользователем, может быть довольно идентифицирующим. У многих компаний есть собственные корпоративные шрифты, которые устанавливаются на ноутбуки сотрудников. Например, у Google есть фирменный шрифт Google Sans.
Злоумышленник может попытаться определить, в какой компании кто-то работает, проверив наличие большого количества известных корпоративных шрифтов, таких как Google Sans. Злоумышленник попытается отобразить текст, заданный этими шрифтами, на холсте и измерить глифы. Если глифы соответствуют известной форме корпоративного шрифта, злоумышленник попадает в цель. Если глифы не совпадают, злоумышленник знает, что использовался заменяющий шрифт по умолчанию, поскольку корпоративный шрифт не был установлен. Для получения полной информации об этой и других атаках с использованием fingerprint в браузере прочтите эту статью.
API для локального доступа к шрифтам
Начало этой статьи могло вызвать у вас плохое настроение. Неужели у нас действительно нет хороших вещей? Не волнуйтесь. Мы думаем, что сможем, и, возможно, все не безнадежно. Но сначала позвольте мне ответить на вопрос, который вы, возможно, задаете себе.
Зачем нам нужен Local Font Access API, когда есть веб-шрифты?
Инструменты для дизайна и графики профессионального качества исторически было сложно разместить в Интернете. Одним из камней преткновения была невозможность получить доступ и использовать все разнообразие профессионально созданных шрифтов с подсказками, которые дизайнеры установили локально. Веб-шрифты позволяют использовать некоторые варианты использования для публикации, но не обеспечивают программный доступ к формам векторных глифов и таблицам шрифтов, используемым растеризаторами для визуализации контуров глифов. Также нет возможности получить доступ к двоичным данным веб-шрифта.
- Инструментам дизайна необходим доступ к байтам шрифта, чтобы выполнять собственную реализацию макета OpenType и позволять инструментам дизайна подключаться на более низких уровнях для таких действий, как выполнение векторных фильтров или преобразование форм глифов.
- У разработчиков могут быть устаревшие стеки шрифтов для своих приложений, которые они переносят в Интернет. Чтобы использовать эти стеки, им обычно требуется прямой доступ к данным шрифтов, чего не предоставляют веб-шрифты.
- Некоторые шрифты могут быть недоступны для доставки через Интернет. Например, Linotype имеет лицензию на некоторые шрифты, которая включает использование только для настольных компьютеров.
API Local Font Access - это попытка решить эти проблемы. Он состоит из двух частей:
- Перечисление шрифтов в API, который позволяет пользователям предоставлять доступ к полному набору доступных шрифтов системы.
- Из каждого результата перечисления - возможность запрашивать низкоуровневый (побайтовый) доступ к контейнеру SFNT, который включает полные данные шрифта.
Как использовать API локального доступа к шрифтам
Включение через chrome://flags
Чтобы поэкспериментировать с Local Font Access API локально, включите флаг #font-access
в chrome://flags
.
Обнаружение функции
Чтобы проверить, поддерживается ли Local Font Access API, используйте:
if ('fonts' in navigator) {
// The Local Font Access API is supported
}
Спрашиваем разрешения
Доступ к локальным шрифтам пользователя ограничен разрешением local-fonts
, которое вы можете запросить с помощью navigator.permissions.request()
.
// Ask for permission to use the API
try {
const status = await navigator.permissions.request({
name: 'local-fonts',
});
if (status.state !== 'granted') {
throw new Error('Permission to access local fonts not granted.');
}
} catch(err) {
// A `TypeError` indicates the 'local-fonts'
// permission is not yet implemented, so
// only `throw` if this is _not_ the problem.
if (err.name !== 'TypeError') {
throw err;
}
}
Предупреждение. На этой ранней стадии разработки API доступа к локальным шрифтам указанное выше разрешение еще не реализовано. В то же время запрос разрешения появится в момент начала перечисления шрифтов. Это поведение было реализовано на crbug.com/1112552.
Перечисление локальных шрифтов
Как только разрешение будет предоставлено, вы можете из интерфейса FontManager
, который отображается в navigator.fonts
, вызвать query()
чтобы запросить у браузера локально установленные шрифты. В результате получается асинхронный итератор, который можно перебрать в for await...of
. Каждый шрифт представлен как объект FontMetadata
со свойствами family
(например, "Comic Sans MS"
), fullName
(например, "Comic Sans MS"
) и postscriptName
(например, "ComicSansMS"
).
// Query for all available fonts and log metadata.
const fonts = navigator.fonts.query();
try {
for await (const metadata of fonts) {
console.log(metadata.postscriptName);
console.log(metadata.fullName);
console.log(metadata.family);
}
} catch (err) {
console.error(err.name, err.message);
}
Доступ к данным SFNT
Полный доступ к SFNT доступен через метод blob()
объекта FontMetadata
. SFNT - это формат файла шрифта, который может содержать другие шрифты, такие как PostScript, TrueType, OpenType, шрифты Web Open Font Format (WOFF) и другие.
const fonts = navigator.fonts.query();
try {
for await (const metadata of fonts) {
// We're only interested in a particular font.
if (metadata.family !== 'Comic Sans MS') {
continue;
}
// `blob()` returns a Blob containing valid and complete
// SFNT-wrapped font data.
const sfnt = await metadata.blob();
const sfntVersion = (new TextDecoder).decode(
// Slice out only the bytes we need: the first 4 bytes are the SFNT
// version info.
// Spec: https://docs.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font
await sfnt.slice(0, 4).arrayBuffer());
let outlineFormat = 'UNKNOWN';
switch (sfntVersion) {
case '\x00\x01\x00\x00':
case 'true':
case 'typ1':
outlineFormat = 'truetype';
break;
case 'OTTO':
outlineFormat = 'cff';
break;
}
console.log('Outline format:', outlineFormat);
}
} catch (err) {
console.error(err.name, err.message);
}
Демо
Вы можете увидеть в действии Local Font Access API в демонстрации ниже. Обязательно ознакомьтесь с исходным кодом. Демонстрация демонстрирует настраиваемый элемент <font-select>
, который реализует выбор локального шрифта.