Собственный конвертер шрифтов

Начните следующий проект с подготовки набора шрифтов.

Время чтения: 8 мин

Задача

Секция статьи "Задача"

В работе разработчика много рутинных задач. Например, подготовка шрифтов. В зависимости от дизайна, требований к скорости загрузки, возможной поддержки нескольких языков и прочих условий, эта работа может занимать много времени.

Существует множество практических советов, как реализовать эту задачу максимально круто. Но все они сводятся к большому количеству рутинных действий. Причём ежедневно подготовкой шрифтов никто не занимается. Нужно время на то, чтобы вспомнить, как использовать инструменты, наверняка пару раз что-то пойдёт не так. Я предлагаю максимально сосредоточиться на процессе разработки, а не на скучных и однообразных действиях. Если есть возможность что-то автоматизировать, то я удержаться не могу.

Готовое решение

Секция статьи "Готовое решение"

Вам понадобится:

  1. Терминал, который поддерживает команды Unix-подобных операционных систем: терминал на macOS или Linux, WSL под Windows.
  2. Node.js — среда выполнения для языка JavaScript, о которой подробнее написано в статье «Что такое Node.js».
  3. Glyphhanger — популярный инструмент для работы со шрифтами, который можно установить командой npm install -g glyphhanger.

Я использую заранее заготовленный скрипт для проектов. Например, если в проекте нужно использовать несколько вариаций шрифтов Fira с поддержкой латиницы, кириллицы, кода и математических выражений, я использую вот такой скрипт font.sh:

        
          
          # Основной скрипт для генерации файлов шрифтовGENERATE_FONT() {  for i in "${!SUBSETNAMES[@]}"; do    for j in "${!FORMATS[@]}"; do      for k in "${!FILENAMES[@]}"; do        INPUT="$INPUTPATH/${FILENAMES[k]}-subset.${FORMATS[j]}"        OUTPUT="$OUTPUTPATH/${FILENAMES[k]}-${SUBSETNAMES[i]}.${FORMATS[j]}"        echo "----------------\n$INPUT -> $OUTPUT\n----------------\n"        glyphhanger --whitelist="${SUBSETCODES[i]}" --formats="${FORMATS[j]}" --subset="$INPUTPATH/${FILENAMES[k]}.$EXTENSION" --css        mv $(echo "$INPUT") $(echo "$OUTPUT")      done    done  done}# Глобальные переменные# Набор названий для сабсетовSUBSETNAMES=("Latin" "LatinSupplement" "LatinExtendedA" "LatinExtendedB" "GreekCoptic" "Cyrilic" "CyrilicSupplement")# Набор диапазонов кодов глифов для каждого из сабсетовSUBSETCODES=("0000−007F" "0080−00FF" "0100−017F" "0180−024F" "0370−03FF" "0400−04FF" "0500−052F")FORMATS=("woff" "woff2")# Набор значений переменных для шрифта FiraSansFILENAMES=("FiraSans-Black" "FiraSans-BlackItalic" "FiraSans-Bold" "FiraSans-BoldItalic" "FiraSans-ExtraBold" "FiraSans-ExtraBoldItalic" "FiraSans-ExtraLight" "FiraSans-LightItalic" "FiraSans-Italic" "FiraSans-Light" "FiraSans-LightItalic" "FiraSans-Medium" "FiraSans-MediumItalic" "FiraSans-Regular" "FiraSans-SemiBold" "FiraSans-SemiBoldItalic" "FiraSans-Thin" "FiraSans-ThinItalic")EXTENSION="ttf"INPUTPATH="./service/fonts/Fira-Sans"OUTPUTPATH="./src/fonts/Fira-Sans"GENERATE_FONT# Набор значений переменных для шрифта FiraCodeFILENAMES=("FiraCode-Bold" "FiraCode-Light" "FiraCode-Medium" "FiraCode-Regular" "FiraCode-SemiBold")EXTENSION="ttf"INPUTPATH="./service/fonts/Fira-Code"OUTPUTPATH="./src/fonts/Fira-Code"GENERATE_FONT# Набор значений переменных для шрифта FiraMathFILENAMES=("FiraMath-Regular")EXTENSION="otf"INPUTPATH="./service/fonts/Fira-Math"OUTPUTPATH="./src/fonts/Fira-Math"GENERATE_FONT
          # Основной скрипт для генерации файлов шрифтов
GENERATE_FONT() {
  for i in "${!SUBSETNAMES[@]}"; do
    for j in "${!FORMATS[@]}"; do
      for k in "${!FILENAMES[@]}"; do
        INPUT="$INPUTPATH/${FILENAMES[k]}-subset.${FORMATS[j]}"
        OUTPUT="$OUTPUTPATH/${FILENAMES[k]}-${SUBSETNAMES[i]}.${FORMATS[j]}"
        echo "----------------\n$INPUT -> $OUTPUT\n----------------\n"
        glyphhanger --whitelist="${SUBSETCODES[i]}" --formats="${FORMATS[j]}" --subset="$INPUTPATH/${FILENAMES[k]}.$EXTENSION" --css
        mv $(echo "$INPUT") $(echo "$OUTPUT")
      done
    done
  done
}

# Глобальные переменные
# Набор названий для сабсетов
SUBSETNAMES=("Latin" "LatinSupplement" "LatinExtendedA" "LatinExtendedB" "GreekCoptic" "Cyrilic" "CyrilicSupplement")

# Набор диапазонов кодов глифов для каждого из сабсетов
SUBSETCODES=("0000−007F" "0080−00FF" "0100−017F" "0180−024F" "0370−03FF" "0400−04FF" "0500−052F")
FORMATS=("woff" "woff2")

# Набор значений переменных для шрифта FiraSans
FILENAMES=("FiraSans-Black" "FiraSans-BlackItalic" "FiraSans-Bold" "FiraSans-BoldItalic" "FiraSans-ExtraBold" "FiraSans-ExtraBoldItalic" "FiraSans-ExtraLight" "FiraSans-LightItalic" "FiraSans-Italic" "FiraSans-Light" "FiraSans-LightItalic" "FiraSans-Medium" "FiraSans-MediumItalic" "FiraSans-Regular" "FiraSans-SemiBold" "FiraSans-SemiBoldItalic" "FiraSans-Thin" "FiraSans-ThinItalic")
EXTENSION="ttf"
INPUTPATH="./service/fonts/Fira-Sans"
OUTPUTPATH="./src/fonts/Fira-Sans"
GENERATE_FONT

# Набор значений переменных для шрифта FiraCode
FILENAMES=("FiraCode-Bold" "FiraCode-Light" "FiraCode-Medium" "FiraCode-Regular" "FiraCode-SemiBold")
EXTENSION="ttf"
INPUTPATH="./service/fonts/Fira-Code"
OUTPUTPATH="./src/fonts/Fira-Code"
GENERATE_FONT

# Набор значений переменных для шрифта FiraMath
FILENAMES=("FiraMath-Regular")
EXTENSION="otf"
INPUTPATH="./service/fonts/Fira-Math"
OUTPUTPATH="./src/fonts/Fira-Math"
GENERATE_FONT

        
        
          
        
      

Такой скрипт позволяет любому участнику проекта быстро сгенерировать или обновить набор шрифтов проекта. Исходники лежат в _ ./service/fonts/<Название-Шрифта>, скрипт создаст нужные шрифты и положит в папку _ ./src/fonts/<Название-Шрифта>.

Установите права на исполнение:

        
          
          chmod 755 font.sh
          chmod 755 font.sh

        
        
          
        
      

Запустить скрипт можно так:

        
          
          sh font.sh
          sh font.sh

        
        
          
        
      

Разбор решения

Секция статьи "Разбор решения"

Обычно формирование набора шрифтов происходит по следующему алгоритму:

  1. Анализ контента сайта на предмет выбора наборов символов с точки зрения максимальной оптимизации загрузки страницы.
  2. Выделение набора гарнитур по макету сайта
  3. Формирование набора имён для файлов
  4. Загрузка исходных файлов шрифтов с максимальным набором символов
  5. Генерация набора файлов шрифтов на основе исходных файлов

Набор символов определяется на основе контента. Стремиться нужно к тому, чтобы загружался только набор тех символов и шрифтов, которые реально используются на странице. Для определения набора символов можно использовать всё ту же утилиту Glyphhanger:

        
          
          glyphhanger http://doka.guide/
          glyphhanger http://doka.guide/

        
        
          
        
      

Так можно узнать, какие символы используются на странице сайта http://doka.guide/. Локальные файлы тоже можно посмотреть:

        
          
          glyphhanger ./test.html
          glyphhanger ./test.html

        
        
          
        
      

Принятым стандартом для формирования файлов шрифтов является использование таблицы символов UTF-8 или UTF-16. Каждый символ обозначается кодом в формате шестнадцатеричного числа. Диапазоны записываются через тире. Есть наборы символов, которые в основном встречаются в текстах на том или ином языке. Можно использовать уже определённые заранее наборы через закреплённые названия в соответствии со стандартами. Именно такой подход используется в моём скрипте. Есть неплохой сайт, чтобы наглядно увидеть разные наборы символов и соответствующие им названия.

Основа скрипта

Секция статьи "Основа скрипта"

Основой для работы является функция, которая генерирует набор файлов с помощью утилиты Glyphhanger:

        
          
          # Функция для генерации шрифтовGENERATE_FONT() {  # Цикл для прохода по всем именам сабсетов  for i in "${!SUBSETNAMES[@]}"; do    # Цикл для прохода по всем форматам    for j in "${!FORMATS[@]}"; do      # Цикл для прохода по всем именам файлов шрифтов      for k in "${!FILENAMES[@]}"; do        # Формирование имени файла на входе        INPUT="$INPUTPATH/${FILENAMES[k]}-subset.${FORMATS[j]}"        # Формирование имени файла на выходе        OUTPUT="$OUTPUTPATH/${FILENAMES[k]}-${SUBSETNAMES[i]}.${FORMATS[j]}"        # Вывод в терминал информации о сформированных файлах        echo "----------------\n$INPUT -> $OUTPUT\n----------------\n"        # Генерация шрифта с помощью glyphhanger        glyphhanger --whitelist="${SUBSETCODES[i]}" --formats="${FORMATS[j]}" --subset="$INPUTPATH/${FILENAMES[k]}.$EXTENSION" --css        # Перемещение сформированного файла в новое место        mv $(echo "$INPUT") $(echo "$OUTPUT")      done    done  done}
          # Функция для генерации шрифтов
GENERATE_FONT() {

  # Цикл для прохода по всем именам сабсетов
  for i in "${!SUBSETNAMES[@]}"; do

    # Цикл для прохода по всем форматам
    for j in "${!FORMATS[@]}"; do

      # Цикл для прохода по всем именам файлов шрифтов
      for k in "${!FILENAMES[@]}"; do

        # Формирование имени файла на входе
        INPUT="$INPUTPATH/${FILENAMES[k]}-subset.${FORMATS[j]}"

        # Формирование имени файла на выходе
        OUTPUT="$OUTPUTPATH/${FILENAMES[k]}-${SUBSETNAMES[i]}.${FORMATS[j]}"

        # Вывод в терминал информации о сформированных файлах
        echo "----------------\n$INPUT -> $OUTPUT\n----------------\n"

        # Генерация шрифта с помощью glyphhanger
        glyphhanger --whitelist="${SUBSETCODES[i]}" --formats="${FORMATS[j]}" --subset="$INPUTPATH/${FILENAMES[k]}.$EXTENSION" --css

        # Перемещение сформированного файла в новое место
        mv $(echo "$INPUT") $(echo "$OUTPUT")
      done
    done
  done
}

        
        
          
        
      

Вы всегда сможете вместо этой утилиты использовать какую-то другую. По аналогии, можно автоматически сжать картинки или подготовить картинки в разных форматах и размерах с помощью Squoosh CLI.

Подходы к реализации

Секция статьи "Подходы к реализации"

Такую функцию можно использовать в интерактивных скриптах, которые взаимодействуют с командной строкой. Достаточно считать аргументы, в которых передаются значения, например, имена директорий для исходных и конечных файлов. Обработать аргументы можно так:

        
          
          # Формирует пустой массив для аргументовPOSITIONAL_ARGS=()# Запускает цикл прохода по всем аргументамwhile [[ $# -gt 0 ]]; do  case $1 in    # Считывание первого аргумента по короткому ключу -i или длинному ключу --input-path    -i|--input-path)      INPUT_PATH="$2"      shift # ключ      shift # значение      ;;    # Считывание первого аргумента по короткому ключу -o или длинному ключу --output-path    -o|--output-path)      OUPUT_PATH="$2"      shift # ключ      shift # значение      ;;    # Обработка ситуации, когда ключ неизвестен    -*|--*)      echo "Неизвестный ключ $1"      exit 1      ;;    *)      # Сохранение значений      POSITIONAL_ARGS+=("$1") # сохранить значения аргументов      shift # последний аргумент      ;;  esacdone# восстановление позиции аргументовset -- "${POSITIONAL_ARGS[@]}"# Для примера просто выведем аргументы на экранecho "INPUT_PATH = ${INPUT_PATH}"echo "OUPUT_PATH = ${OUPUT_PATH}"# Если аргумент не известен, то надо об этом рассказатьif [[ -n $1 ]]; then    echo "Аргумент не известен:"    tail -1 "$1"fi
          # Формирует пустой массив для аргументов
POSITIONAL_ARGS=()

# Запускает цикл прохода по всем аргументам
while [[ $# -gt 0 ]]; do
  case $1 in

    # Считывание первого аргумента по короткому ключу -i или длинному ключу --input-path
    -i|--input-path)
      INPUT_PATH="$2"
      shift # ключ
      shift # значение
      ;;

    # Считывание первого аргумента по короткому ключу -o или длинному ключу --output-path
    -o|--output-path)
      OUPUT_PATH="$2"
      shift # ключ
      shift # значение
      ;;

    # Обработка ситуации, когда ключ неизвестен
    -*|--*)
      echo "Неизвестный ключ $1"
      exit 1
      ;;
    *)

      # Сохранение значений
      POSITIONAL_ARGS+=("$1") # сохранить значения аргументов
      shift # последний аргумент
      ;;
  esac
done

# восстановление позиции аргументов
set -- "${POSITIONAL_ARGS[@]}"

# Для примера просто выведем аргументы на экран
echo "INPUT_PATH = ${INPUT_PATH}"
echo "OUPUT_PATH = ${OUPUT_PATH}"

# Если аргумент не известен, то надо об этом рассказать
if [[ -n $1 ]]; then
    echo "Аргумент не известен:"
    tail -1 "$1"
fi

        
        
          
        
      

Теперь можно запустить командой:

        
          
          sh script.sh -i digits -o alphabet
          sh script.sh -i digits -o alphabet

        
        
          
        
      

Если есть возможность облегчить себе труд в будущем, то надо обязательно это сделать. В будущем вы сэкономите очень много времени.