Blog. Just Blog

Загрузка шрифтов WOFF на Ассемблере

Версия для печати Добавить в Избранное Отправить на E-Mail | Категория: Образ мышления: Assembler | Автор: ManHunter
Загрузка шрифтов WOFF на Ассемблере
Загрузка шрифтов WOFF на Ассемблере

WOFF или Web Open Font Format - формат шрифтов, чаще всего используемый для Web. Он основан на стандартных форматах шрифтов OpenType или TrueType, но данные в WOFF хранятся в сжатом виде, за счет чего повышается скорость загрузки. Штатными средствами система Windows с такими шрифтами работать не умеет, поэтому мне стало интересно разобраться с этим форматом.

Внутреннее устройство формата WOFF хорошо описано на сайте W3C, для удобства я собрал последнюю версию страницы документации в справку CHM. Скачать ее можно по ссылке ниже.

WOFF File Format 1.0 (ENG)WOFF File Format 1.0 (ENG)

WOFF.File.Format.1.0.zip (25,183 bytes)

Для конвертирования шрифтов OTF и TTF в WOFF и обратно есть две консольные утилиты sfnt2woff и woff2sfnt от Jonathan Kew сотоварищи. Офсайт прекратил свое существование, но их исходники успели форкнуть на GitHub. В процессе исследования они здорово помогли прояснить некоторые моменты, как правильно обрабатывать внутренности WOFF. Я скомпилировал обе утилиты в исполняемые файлы, так как найти их в готовом виде оказалось вообще нереально.

Утилиты sfnt2woff и woff2sfntУтилиты sfnt2woff и woff2sfnt

WOFF.Font.Converters.zip (106,741 bytes)

Для лучшего понимания давайте посмотрим на внутренности файла шрифта. Шрифты OpenType и TrueType по структуре очень похожи. Сперва идет заголовок фиксированного размера, после него находится таблица директорий и затем различные данные шрифта, на которые ссылаются указатели из записей этой таблицы.

Структура шрифта OpenType
Структура шрифта OpenType

Важный момент. Порядок следования блоков данных может не совпадать с порядком записей в таблице. Более того, вы можете как угодно менять местами блоки данных при условии, что указатель из директории на соответствующий блок данных будет также иметь правильное значение. Этот факт нам пригодится при дальнейшем исследовании. А так как указатель на данные считается от начала файла, то записи в таблице директорий можно перемешивать в произвольном порядке, если блоки данных при этом останутся без изменений. Это нам нигде не пригодится, но знать будет полезным. Все блоки данных выровнены на границу 4 байт, это обязательное условие.

Внутреннее устройство WOFF по сути точно такое же, как и у обычных шрифтов, за исключением того, что блоки данных сжаты при помощи алгоритма zlib. Соответственно, структура этих файлов обладает теми же свойствами в плане перемещения блоков данных и записей таблицы директорий, что и у неупакованных шрифтов. Для преобразования шрифта из формата WOFF в привычный вид надо восстановить заголовок OTF или TTF шрифта на основании сохраненных в WOFF данных, последовательно пройтись по всем записям таблицы директорий WOFF, заполнив ее в распакованном шрифте, при этом распаковывая блоки данных, на которые ссылаются эти записи.

В следующих структурах описывается формат заголовка WOFF-файла и формат записей таблицы директорий.
  1. struct WOFF_HEADER
  2.         signature      dd ? ; 0x774F4646 'wOFF'
  3.         flavor         dd ? ; Тип исходного шрифта
  4.         length         dd ? ; Полный размер файла WOFF
  5.         numTables      dw ? ; Количество записей в таблице директорий
  6.         reserved       dw ? ; Зарезервировано
  7.         totalSfntSize  dd ? ; Размер файла исходного шрифта
  8.         majorVersion   dw ? ; Старшая версия WOFF файла
  9.         minorVersion   dw ? ; Младшая версия WOFF файла
  10.         metaOffset     dd ? ; Указатель на метаданные от начала WOFF файла
  11.         metaLength     dd ? ; Размер блока сжатых метаданных
  12.         metaOrigLength dd ? ; Оригинальный размер метаданных
  13.         privOffset     dd ? ; Указатель на личные данные от начала WOFF файла
  14.         privLength     dd ? ; Размер блока личных данных
  15. ends
  16.  
  17. struct WOFF_TABLE_DIRECTORY
  18.         tag            dd ? ; Идентификатор записи
  19.         dataOffset     dd ? ; Указатель на данные от начала файла
  20.         compLength     dd ? ; Размер упакованных данных без учета выравнивания
  21.         origLength     dd ? ; Размер оригинальных данных без учета выравнивания
  22.         origChecksum   dd ? ; Контрольная сумма оригинальных данных
  23. ends
Остальные структуры, которые тут будут использоваться, вы можете посмотреть в статье про загрузку шрифтов из памяти. Минимально необходимый размер блока памяти для сохранения распакованного шрифта определяется значением поля totalSfntSize из структуры заголовка WOFF. Распаковку шрифта начинаем с восстановления заголовка оригинального шрифта. Комментарии к операциям соответствуют исходнику утилиты woff2sfnt.
  1.         ; ESI -> указатель на начало данных файла WOFF
  2.         ; EDI -> указатель на буфер-приемник распакованного шрифта
  3.  
  4.         mov     eax,[esi+WOFF_HEADER.flavor]
  5.         mov     dword [edi+TT_OFFSET_TABLE.majorVersion],eax
  6.         mov     ax,[esi+WOFF_HEADER.numTables]
  7.         mov     [edi+TT_OFFSET_TABLE.numOfTables],ax
  8.  
  9.         ; searchRange = numTables;
  10.         movzx   eax,[esi+WOFF_HEADER.numTables]
  11.         xchg    al,ah
  12.         ; searchRange |= (searchRange >> 1);
  13.         mov     ecx,eax
  14.         shr     ecx,1
  15.         or      eax,ecx
  16.         ; searchRange |= (searchRange >> 2);
  17.         mov     ecx,eax
  18.         shr     ecx,2
  19.         or      eax,ecx
  20.         ; searchRange |= (searchRange >> 4);
  21.         mov     ecx,eax
  22.         shr     ecx,4
  23.         or      eax,ecx
  24.         ; searchRange |= (searchRange >> 8);
  25.         mov     ecx,eax
  26.         shr     ecx,8
  27.         or      eax,ecx
  28.         ; searchRange &= ~(searchRange >> 1);
  29.         mov     ecx,eax
  30.         shr     ecx,1
  31.         not     ecx
  32.         and     eax,ecx
  33.         ; searchRange *= 16;
  34.         shl     eax,4
  35.         ; newHeader->searchRange = READ16BE(searchRange);
  36.         mov     ecx,eax
  37.         xchg    cl,ch
  38.         mov     [edi+TT_OFFSET_TABLE.searchRange],cx
  39.  
  40.         ; rangeShift = numTables * 16 - searchRange;
  41.         movzx   ecx,[esi+WOFF_HEADER.numTables]
  42.         xchg    cl,ch
  43.         shl     ecx,4
  44.         sub     ecx,eax
  45.         ; newHeader->rangeShift = READ16BE(rangeShift);
  46.         xchg    cl,ch
  47.         mov     [edi+TT_OFFSET_TABLE.rangeShift],cx
  48.  
  49.         ; entrySelector = 0;
  50.         xor     ecx,ecx
  51.         ; while (searchRange > 16) {
  52.         ;   ++entrySelector;
  53.         ;   searchRange >>= 1;
  54.         ; }
  55. @@:
  56.         cmp     eax,16
  57.         jbe     @f
  58.         inc     ecx
  59.         shr     eax,1
  60.         jmp     @b
  61. @@:
  62.         ; newHeader->entrySelector = READ16BE(entrySelector);
  63.         xchg    cl,ch
  64.         mov     [edi+TT_OFFSET_TABLE.entrySelector],cx
После этого, зная количество записей в таблице директорий WOFF и размер структуры TT_TABLE_DIRECTORY, резервируем место для таблицы директорий в распакованном шрифте. Указатель на память для приема распакованных блоков данных ставим сразу же после этой таблицы.
  1.         ; ESI -> указатель на таблицу директорий WOFF
  2.         ; EDI -> указатель на таблицу директорий распакованного шрифта
  3.         ; dOffs - указатель на распакованные данные
  4.         ; dNum - количество записей в таблице директорий
  5.         ; woff_data -> указатель на начало данных файла WOFF
  6.         ; unpacked_font - указатель на буфер-приемник распакованного шрифта
  7.  
  8.         xor     ecx,ecx
  9. loc_scan:
  10.         push    ecx
  11.  
  12.         ; Заполнить запись в таблице распакованного шрифта
  13.         mov     eax,[esi+WOFF_TABLE_DIRECTORY.tag]
  14.         stosd
  15.         mov     eax,[esi+WOFF_TABLE_DIRECTORY.origChecksum]
  16.         stosd
  17.         mov     eax,[dOffs]
  18.         sub     eax,[unpacked_font]
  19.         bswap   eax
  20.         stosd
  21.         mov     eax,[esi+WOFF_TABLE_DIRECTORY.origLength]
  22.         stosd
  23.  
  24.         ; Распаковать сами данные
  25.         pusha
  26.  
  27.         ; Если размер исходных данных меньше или равен размеру
  28.         ; упакованных, то просто скопировать блок
  29.         mov     eax,[esi+WOFF_TABLE_DIRECTORY.origLength]
  30.         bswap   eax
  31.         mov     ecx,[esi+WOFF_TABLE_DIRECTORY.compLength]
  32.         bswap   ecx
  33.         cmp     ecx,eax
  34.         jae     loc_just_copy
  35.  
  36. loc_uncompress:
  37.         ; Планируемый размер распакованных данных
  38.         mov     [tmp],eax
  39.         ; Указатель на упакованные данные
  40.         mov     eax,[esi+WOFF_TABLE_DIRECTORY.dataOffset]
  41.         bswap   eax
  42.         add     eax,[woff_data]
  43.  
  44.         ; Распаковать данные
  45.         invoke  uncompress,[dOffs],tmp,eax,ecx
  46.         jmp     loc_process_done
  47.  
  48. loc_just_copy:
  49.         ; Размер данных
  50.         mov     ecx,[esi+WOFF_TABLE_DIRECTORY.origLength]
  51.         bswap   ecx
  52.         ; Приемник
  53.         mov     edi,[dOffs]
  54.         ; Источник
  55.         mov     esi,[esi+WOFF_TABLE_DIRECTORY.dataOffset]
  56.         bswap   esi
  57.         add     esi,[woff_data]
  58.         ; Скопировать данные
  59.         rep     movsb
  60.  
  61. loc_process_done:
  62.         popa
  63.  
  64.         ; Перенести указатель в конец распакованных данных
  65.         mov     eax,[esi+WOFF_TABLE_DIRECTORY.origLength]
  66.         bswap   eax
  67.         ; Выравнивание до 4 байт
  68.         add     eax,3
  69.         and     eax,0xFFFFFFFC
  70.         add     [dOffs],eax
  71.  
  72. loc_next:
  73.         ; Следующая запись в таблице
  74.         pop     ecx
  75.         add     esi,sizeof.WOFF_TABLE_DIRECTORY
  76.  
  77.         ; Все записи обработаны?
  78.         inc     ecx
  79.         cmp     cx,[dNum]
  80.         jb      loc_scan
В процессе распаковки блоки данных выстраиваются в том порядке, в котором следуют соответствующие им записи в таблице директорий. Как я уже обращал ваше внимание, на корректность шрифта это не повлияет. Если стоит задача, чтобы распакованный WOFF-файл байт-в-байт совпадал с оригинальным файлом, то придется поочередно сканировать смещения упакованных блоков и на основании этих значений искать и восстанавливать соответствующую им запись в таблице директорий. Задача вполне решаемая, но заметно усложняющая обработку файла. Еще один важный момент. Обязательно надо сравнивать значения из полей origLength и compLength структуры WOFF_TABLE_DIRECTORY, если размер упакованных данных не меньше размера оригинальных, то данные в этом блоке хранятся в неупакованном виде и их надо просто скопировать. Так бывает, например, с очень маленькими блоками данных, когда упаковка не дает никакого выигрыша. Ну и не забываем про выравнивание распакованных блоков на границу 4 байт.
  1.         ; ESI -> указатель на таблицу директорий WOFF
  2.         ; dOffs - указатель на распакованные данные
  3.         ; pOffs - указатель на упакованные данные
  4.         ; dNum - количество записей в таблице директорий
  5.         ; woff_data -> указатель на начало данных файла WOFF
  6.         ; unpacked_font - указатель на буфер-приемник распакованного шрифта
  7.  
  8.         xor     ecx,ecx
  9. loc_scan:
  10.         push    ecx
  11.  
  12.         xor     ecx,ecx
  13.         mov     esi,[woff_data]
  14.         add     esi,sizeof.WOFF_HEADER
  15.  
  16. loc_scan_sub:
  17.         push    ecx
  18.  
  19.         ; Указатель записи соответствует текущему значению?
  20.         mov     eax,[esi+WOFF_TABLE_DIRECTORY.dataOffset]
  21.         bswap   eax
  22.         cmp     eax,[pOffs]
  23.         jne     loc_next_sub
  24.  
  25.         ; Указатель на обрабатываемую запись в таблице
  26.         xor     edx,edx
  27.         pop     eax
  28.         mov     ecx,sizeof.TT_TABLE_DIRECTORY
  29.         mul     ecx
  30.  
  31.         ; Указатель на таблицу распакованных данных
  32.         mov     edi,[unpacked_font]
  33.         add     edi,sizeof.TT_OFFSET_TABLE
  34.         add     edi,eax
  35.  
  36.         ; Заполнить запись в таблице распакованных данных
  37.         mov     eax,[esi+WOFF_TABLE_DIRECTORY.tag]
  38.         stosd
  39.         mov     eax,[esi+WOFF_TABLE_DIRECTORY.origChecksum]
  40.         stosd
  41.         mov     eax,[dOffs]
  42.         sub     eax,[unpacked_font]
  43.         bswap   eax
  44.         stosd
  45.         mov     eax,[esi+WOFF_TABLE_DIRECTORY.origLength]
  46.         stosd
  47.  
  48.         ; Распаковать сами данные
  49.         pusha
  50.  
  51.         ; Если размер исходных данных меньше или равен размеру
  52.         ; упакованных, то просто скопировать блок
  53.         mov     eax,[esi+WOFF_TABLE_DIRECTORY.origLength]
  54.         bswap   eax
  55.         mov     ecx,[esi+WOFF_TABLE_DIRECTORY.compLength]
  56.         bswap   ecx
  57.         cmp     ecx,eax
  58.         jae     loc_just_copy
  59.  
  60. loc_uncompress:
  61.         ; Планируемый размер распакованных данных
  62.         mov     [tmp],eax
  63.         ; Указатель на упакованные данные
  64.         mov     eax,[pOffs]
  65.         add     eax,[woff_data]
  66.         ; Распаковать данные
  67.         invoke  uncompress,[dOffs],tmp,eax,ecx
  68.         jmp     loc_uncompress_done
  69. loc_just_copy:
  70.         ; Размер данных
  71.         mov     ecx,[esi+WOFF_TABLE_DIRECTORY.origLength]
  72.         bswap   ecx
  73.         ; Приемник
  74.         mov     edi,[dOffs]
  75.         ; Источник
  76.         mov     esi,[woff_data]
  77.         add     esi,[pOffs]
  78.         ; Скопировать данные
  79.         rep     movsb
  80.  
  81. loc_uncompress_done:
  82.         popa
  83.  
  84.         ; Следующее смещение распакованных данных
  85.         mov     eax,[esi+WOFF_TABLE_DIRECTORY.origLength]
  86.         bswap   eax
  87.         ; Выравнивание до 4 байт
  88.         add     eax,3
  89.         and     eax,0xFFFFFFFC
  90.         add     [dOffs],eax
  91.  
  92.         ; Следующее смещение упакованных данных
  93.         mov     eax,[esi+WOFF_TABLE_DIRECTORY.compLength]
  94.         bswap   eax
  95.         ; Выравнивание до 4 байт
  96.         add     eax,3
  97.         and     eax,0xFFFFFFFC
  98.         add     [pOffs],eax
  99.  
  100.         jmp     loc_next_dir
  101.  
  102. loc_next_sub:
  103.         ; Следующая запись в таблице
  104.         add     esi,sizeof.WOFF_TABLE_DIRECTORY
  105.         ; Все блоки данных обработаны?
  106.         pop     ecx
  107.         inc     ecx
  108.         cmp     cx,[dNum]
  109.         jb      loc_scan_sub
  110.  
  111. loc_next_dir:
  112.         ; Все блоки данных обработаны?
  113.         pop     ecx
  114.         inc     ecx
  115.         cmp     cx,[dNum]
  116.         jb      loc_scan
Всякие метаданные и личные данные из WOFF-шрифтов к оригинальным шрифтам никаким боком не относятся и при обработке должны быть проигнорированы.

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

Сейчас популярен новый формат сжатых шрифтов WOFF2, в котором степень компрессии еще выше (в эпоху безлимитных интернетов, мегабайтных каналов и всяких CDN, ага). Но там используется какой-то нестандартный алгоритм компрессии Brotli, для которого компактных решений, тем более на Ассемблере, найти не удалось. Официальные гугловские утилиты у меня собрать тоже не получилось, а монстрообразные конвертеры из пакета Cygwin на выходе дают некорректный результат. Поэтому про WOFF2 мне рассказать нечего.

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

Примеры программ с исходными текстами (FASM)Примеры программ с исходными текстами (FASM)

WOFF.Unpack.Demo.zip (132,245 bytes)


Поделиться ссылкой ВКонтакте
Просмотров: 687 | Комментариев: 2

Внимание! Статья опубликована больше года назад, информация могла устареть!

Комментарии

Отзывы посетителей сайта о статье
Гость (13.12.2022 в 17:56):
оказывается офиц. сайт просто недоступен из россии: https://bayden.com/dl/brotli.exe, надеюсь пригодится, можно удалить все эти уже ненужные комментарии
Гость (13.12.2022 в 17:30):
Я вообще обалдел узнав про какой-то Brotli, пытаясь разобрать ответ от сервера, может кто-то подскажет что-нибудь получше или даст ссылку на офиц. Brotli.exe, а пока вот это есть https://github.com/VarunSaiTej...ger/releases

Добавить комментарий

Заполните форму для добавления комментария
Имя*:
Текст комментария (не более 2000 символов)*:

*Все поля обязательны для заполнения.
Комментарии, содержащие рекламу, ненормативную лексику, оскорбления и т.п., а также флуд и сообщения не по теме, будут удаляться. Нарушителям может быть заблокирован доступ к сайту.
Наверх
Powered by PCL's Speckled Band Engine 0.2 RC3
© ManHunter / PCL, 2008-2024
При использовании материалов ссылка на сайт обязательна
Время генерации: 0.07 сек. / MySQL: 2 (0.0061 сек.) / Память: 4.5 Mb
Наверх