Загрузка шрифтов WOFF на Ассемблере
Загрузка шрифтов WOFF на Ассемблере
WOFF или Web Open Font Format - формат шрифтов, чаще всего используемый для Web. Он основан на стандартных форматах шрифтов OpenType или TrueType, но данные в WOFF хранятся в сжатом виде, за счет чего повышается скорость загрузки. Штатными средствами система Windows с такими шрифтами работать не умеет, поэтому мне стало интересно разобраться с этим форматом.
Внутреннее устройство формата WOFF хорошо описано на сайте W3C, для удобства я собрал последнюю версию страницы документации в справку CHM. Скачать ее можно по ссылке ниже.
Для конвертирования шрифтов OTF и TTF в WOFF и обратно есть две консольные утилиты sfnt2woff и woff2sfnt от Jonathan Kew сотоварищи. Офсайт прекратил свое существование, но их исходники успели форкнуть на GitHub. В процессе исследования они здорово помогли прояснить некоторые моменты, как правильно обрабатывать внутренности WOFF. Я скомпилировал обе утилиты в исполняемые файлы, так как найти их в готовом виде оказалось вообще нереально.
Для лучшего понимания давайте посмотрим на внутренности файла шрифта. Шрифты OpenType и TrueType по структуре очень похожи. Сперва идет заголовок фиксированного размера, после него находится таблица директорий и затем различные данные шрифта, на которые ссылаются указатели из записей этой таблицы.
Структура шрифта OpenType
Важный момент. Порядок следования блоков данных может не совпадать с порядком записей в таблице. Более того, вы можете как угодно менять местами блоки данных при условии, что указатель из директории на соответствующий блок данных будет также иметь правильное значение. Этот факт нам пригодится при дальнейшем исследовании. А так как указатель на данные считается от начала файла, то записи в таблице директорий можно перемешивать в произвольном порядке, если блоки данных при этом останутся без изменений. Это нам нигде не пригодится, но знать будет полезным. Все блоки данных выровнены на границу 4 байт, это обязательное условие.
Внутреннее устройство WOFF по сути точно такое же, как и у обычных шрифтов, за исключением того, что блоки данных сжаты при помощи алгоритма zlib. Соответственно, структура этих файлов обладает теми же свойствами в плане перемещения блоков данных и записей таблицы директорий, что и у неупакованных шрифтов. Для преобразования шрифта из формата WOFF в привычный вид надо восстановить заголовок OTF или TTF шрифта на основании сохраненных в WOFF данных, последовательно пройтись по всем записям таблицы директорий WOFF, заполнив ее в распакованном шрифте, при этом распаковывая блоки данных, на которые ссылаются эти записи.
В следующих структурах описывается формат заголовка WOFF-файла и формат записей таблицы директорий.
Code (Assembler) : Убрать нумерацию
- struct WOFF_HEADER
- signature dd ? ; 0x774F4646 'wOFF'
- flavor dd ? ; Тип исходного шрифта
- length dd ? ; Полный размер файла WOFF
- numTables dw ? ; Количество записей в таблице директорий
- reserved dw ? ; Зарезервировано
- totalSfntSize dd ? ; Размер файла исходного шрифта
- majorVersion dw ? ; Старшая версия WOFF файла
- minorVersion dw ? ; Младшая версия WOFF файла
- metaOffset dd ? ; Указатель на метаданные от начала WOFF файла
- metaLength dd ? ; Размер блока сжатых метаданных
- metaOrigLength dd ? ; Оригинальный размер метаданных
- privOffset dd ? ; Указатель на личные данные от начала WOFF файла
- privLength dd ? ; Размер блока личных данных
- ends
- struct WOFF_TABLE_DIRECTORY
- tag dd ? ; Идентификатор записи
- dataOffset dd ? ; Указатель на данные от начала файла
- compLength dd ? ; Размер упакованных данных без учета выравнивания
- origLength dd ? ; Размер оригинальных данных без учета выравнивания
- origChecksum dd ? ; Контрольная сумма оригинальных данных
- ends
Code (Assembler) : Убрать нумерацию
- ; ESI -> указатель на начало данных файла WOFF
- ; EDI -> указатель на буфер-приемник распакованного шрифта
- mov eax,[esi+WOFF_HEADER.flavor]
- mov dword [edi+TT_OFFSET_TABLE.majorVersion],eax
- mov ax,[esi+WOFF_HEADER.numTables]
- mov [edi+TT_OFFSET_TABLE.numOfTables],ax
- ; searchRange = numTables;
- movzx eax,[esi+WOFF_HEADER.numTables]
- xchg al,ah
- ; searchRange |= (searchRange >> 1);
- mov ecx,eax
- shr ecx,1
- or eax,ecx
- ; searchRange |= (searchRange >> 2);
- mov ecx,eax
- shr ecx,2
- or eax,ecx
- ; searchRange |= (searchRange >> 4);
- mov ecx,eax
- shr ecx,4
- or eax,ecx
- ; searchRange |= (searchRange >> 8);
- mov ecx,eax
- shr ecx,8
- or eax,ecx
- ; searchRange &= ~(searchRange >> 1);
- mov ecx,eax
- shr ecx,1
- not ecx
- and eax,ecx
- ; searchRange *= 16;
- shl eax,4
- ; newHeader->searchRange = READ16BE(searchRange);
- mov ecx,eax
- xchg cl,ch
- mov [edi+TT_OFFSET_TABLE.searchRange],cx
- ; rangeShift = numTables * 16 - searchRange;
- movzx ecx,[esi+WOFF_HEADER.numTables]
- xchg cl,ch
- shl ecx,4
- sub ecx,eax
- ; newHeader->rangeShift = READ16BE(rangeShift);
- xchg cl,ch
- mov [edi+TT_OFFSET_TABLE.rangeShift],cx
- ; entrySelector = 0;
- xor ecx,ecx
- ; while (searchRange > 16) {
- ; ++entrySelector;
- ; searchRange >>= 1;
- ; }
- @@:
- cmp eax,16
- jbe @f
- inc ecx
- shr eax,1
- jmp @b
- @@:
- ; newHeader->entrySelector = READ16BE(entrySelector);
- xchg cl,ch
- mov [edi+TT_OFFSET_TABLE.entrySelector],cx
Code (Assembler) : Убрать нумерацию
- ; ESI -> указатель на таблицу директорий WOFF
- ; EDI -> указатель на таблицу директорий распакованного шрифта
- ; dOffs - указатель на распакованные данные
- ; dNum - количество записей в таблице директорий
- ; woff_data -> указатель на начало данных файла WOFF
- ; unpacked_font - указатель на буфер-приемник распакованного шрифта
- xor ecx,ecx
- loc_scan:
- push ecx
- ; Заполнить запись в таблице распакованного шрифта
- mov eax,[esi+WOFF_TABLE_DIRECTORY.tag]
- stosd
- mov eax,[esi+WOFF_TABLE_DIRECTORY.origChecksum]
- stosd
- mov eax,[dOffs]
- sub eax,[unpacked_font]
- bswap eax
- stosd
- mov eax,[esi+WOFF_TABLE_DIRECTORY.origLength]
- stosd
- ; Распаковать сами данные
- pusha
- ; Если размер исходных данных меньше или равен размеру
- ; упакованных, то просто скопировать блок
- mov eax,[esi+WOFF_TABLE_DIRECTORY.origLength]
- bswap eax
- mov ecx,[esi+WOFF_TABLE_DIRECTORY.compLength]
- bswap ecx
- cmp ecx,eax
- jae loc_just_copy
- loc_uncompress:
- ; Планируемый размер распакованных данных
- mov [tmp],eax
- ; Указатель на упакованные данные
- mov eax,[esi+WOFF_TABLE_DIRECTORY.dataOffset]
- bswap eax
- add eax,[woff_data]
- ; Распаковать данные
- invoke uncompress,[dOffs],tmp,eax,ecx
- jmp loc_process_done
- loc_just_copy:
- ; Размер данных
- mov ecx,[esi+WOFF_TABLE_DIRECTORY.origLength]
- bswap ecx
- ; Приемник
- mov edi,[dOffs]
- ; Источник
- mov esi,[esi+WOFF_TABLE_DIRECTORY.dataOffset]
- bswap esi
- add esi,[woff_data]
- ; Скопировать данные
- rep movsb
- loc_process_done:
- popa
- ; Перенести указатель в конец распакованных данных
- mov eax,[esi+WOFF_TABLE_DIRECTORY.origLength]
- bswap eax
- ; Выравнивание до 4 байт
- add eax,3
- and eax,0xFFFFFFFC
- add [dOffs],eax
- loc_next:
- ; Следующая запись в таблице
- pop ecx
- add esi,sizeof.WOFF_TABLE_DIRECTORY
- ; Все записи обработаны?
- inc ecx
- cmp cx,[dNum]
- jb loc_scan
Code (Assembler) : Убрать нумерацию
- ; ESI -> указатель на таблицу директорий WOFF
- ; dOffs - указатель на распакованные данные
- ; pOffs - указатель на упакованные данные
- ; dNum - количество записей в таблице директорий
- ; woff_data -> указатель на начало данных файла WOFF
- ; unpacked_font - указатель на буфер-приемник распакованного шрифта
- xor ecx,ecx
- loc_scan:
- push ecx
- xor ecx,ecx
- mov esi,[woff_data]
- add esi,sizeof.WOFF_HEADER
- loc_scan_sub:
- push ecx
- ; Указатель записи соответствует текущему значению?
- mov eax,[esi+WOFF_TABLE_DIRECTORY.dataOffset]
- bswap eax
- cmp eax,[pOffs]
- jne loc_next_sub
- ; Указатель на обрабатываемую запись в таблице
- xor edx,edx
- pop eax
- mov ecx,sizeof.TT_TABLE_DIRECTORY
- mul ecx
- ; Указатель на таблицу распакованных данных
- mov edi,[unpacked_font]
- add edi,sizeof.TT_OFFSET_TABLE
- add edi,eax
- ; Заполнить запись в таблице распакованных данных
- mov eax,[esi+WOFF_TABLE_DIRECTORY.tag]
- stosd
- mov eax,[esi+WOFF_TABLE_DIRECTORY.origChecksum]
- stosd
- mov eax,[dOffs]
- sub eax,[unpacked_font]
- bswap eax
- stosd
- mov eax,[esi+WOFF_TABLE_DIRECTORY.origLength]
- stosd
- ; Распаковать сами данные
- pusha
- ; Если размер исходных данных меньше или равен размеру
- ; упакованных, то просто скопировать блок
- mov eax,[esi+WOFF_TABLE_DIRECTORY.origLength]
- bswap eax
- mov ecx,[esi+WOFF_TABLE_DIRECTORY.compLength]
- bswap ecx
- cmp ecx,eax
- jae loc_just_copy
- loc_uncompress:
- ; Планируемый размер распакованных данных
- mov [tmp],eax
- ; Указатель на упакованные данные
- mov eax,[pOffs]
- add eax,[woff_data]
- ; Распаковать данные
- invoke uncompress,[dOffs],tmp,eax,ecx
- jmp loc_uncompress_done
- loc_just_copy:
- ; Размер данных
- mov ecx,[esi+WOFF_TABLE_DIRECTORY.origLength]
- bswap ecx
- ; Приемник
- mov edi,[dOffs]
- ; Источник
- mov esi,[woff_data]
- add esi,[pOffs]
- ; Скопировать данные
- rep movsb
- loc_uncompress_done:
- popa
- ; Следующее смещение распакованных данных
- mov eax,[esi+WOFF_TABLE_DIRECTORY.origLength]
- bswap eax
- ; Выравнивание до 4 байт
- add eax,3
- and eax,0xFFFFFFFC
- add [dOffs],eax
- ; Следующее смещение упакованных данных
- mov eax,[esi+WOFF_TABLE_DIRECTORY.compLength]
- bswap eax
- ; Выравнивание до 4 байт
- add eax,3
- and eax,0xFFFFFFFC
- add [pOffs],eax
- jmp loc_next_dir
- loc_next_sub:
- ; Следующая запись в таблице
- add esi,sizeof.WOFF_TABLE_DIRECTORY
- ; Все блоки данных обработаны?
- pop ecx
- inc ecx
- cmp cx,[dNum]
- jb loc_scan_sub
- loc_next_dir:
- ; Все блоки данных обработаны?
- pop ecx
- inc ecx
- cmp cx,[dNum]
- jb loc_scan
После распаковки шрифт можно загрузить напрямую из памяти, как описано по приведенной выше ссылке, или сохранить на диск, или сделать с ним то, что планировалось согласно поставленной задаче.
Сейчас популярен новый формат сжатых шрифтов WOFF2, в котором степень компрессии еще выше (в эпоху безлимитных интернетов, мегабайтных каналов и всяких CDN, ага). Но там используется какой-то нестандартный алгоритм компрессии Brotli, для которого компактных решений, тем более на Ассемблере, найти не удалось. Официальные гугловские утилиты у меня собрать тоже не получилось, а монстрообразные конвертеры из пакета Cygwin на выходе дают некорректный результат. Поэтому про WOFF2 мне рассказать нечего.
В приложении пример программ с исходными текстами, одна из которых просто распаковывает шрифт WOFF до оригинального состояния, а вторая распаковывает и пересобирает шрифт, согласно записям в таблице директорий.
Просмотров: 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
Добавить комментарий
Заполните форму для добавления комментария