Blog. Just Blog

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

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

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

Для начала файл шрифта надо внедрить в наш исполняемый файл. На Ассемблере это делается примерно так, как показано ниже. В дальнейшем нам понадобится размер файла, так что и его сразу посчитаем.
  1. ; Шрифт содержится в исполняемом файле
  2. font_data:
  3. file 'Horrormaster.ttf'
  4. ; Размер файла шрифта
  5. font_size=$-font_data
  6. ; Название шрифта
  7. font_name  db 'Horrormaster',0
Переходим к загрузке нужного нам шрифта из памяти. Она выполняется одной единственной функцией AddFontMemResourceEx. Особенность ее работы заключается в том, что шрифт как бы устанавливается в систему, но фактически это происходит только для вызывающего процесса и только на время его запуска. После успешной загрузки шрифт можно использовать как любой другой системный шрифт.
  1.         ; Загрузить шрифт в память для нашего процесса
  2.         invoke  AddFontMemResourceEx,font_data,font_size,0,tmp
  3.         ; Создать шрифт
  4.         invoke  CreateFont,40,20,0,0,700,FALSE,FALSE,FALSE,\
  5.                 DEFAULT_CHARSET,OUT_RASTER_PRECIS,CLIP_DEFAULT_PRECIS,\
  6.                 PROOF_QUALITY,FIXED_PITCH+FF_DONTCARE,font_name
  7.         ; Установить шрифт элементу окна
  8.         invoke  SendDlgItemMessage,[hwnddlg],ID_TEXT,WM_SETFONT,eax,0
Шрифт можно хранить не только в сегменте данных исполняемого файла, но и в ресурсах, а то и вовсе в какой-нибудь внешней dll. В этом случае придется задействовать функции работы с ресурсами: FindResource, LoadResource и LockResource, чтобы получить адрес шрифта в памяти, а также SizeofResource для получения его размера. Все последующие действия по работе со шрифтом точно такие же.

Для создания шрифта функцией CreateFont надо обязательно указать его название. В случае, когда в вашем приложении используется какой-нибудь строго определенный шрифт, то никаких проблем, вы можете сразу прописать его название в виде строки. А как быть в ситуации, когда шрифт получен из сторонних источников и никакой информации о нем нет. Для шрифтов в виде файлов все решается одной функцией GetFontResourceInfo, правда в MSDN она официально никак не документирована. Но у нас же физического файла нет, есть только данные шрифта в памяти. Значит будем парсить TTF-файлы самостоятельно. Внутренняя структура TTF-файлов отлично описана в TrueType Reference Manual или в аналогичном мануале от Microsoft. Мурзилки вы можете вдумчиво почитать в свободное время, я же сразу перейду к описанию всех необходимых структур, которые понадобятся нам для работы.
  1. ; Заголовок шрифта
  2. struct TT_OFFSET_TABLE
  3.         majorVersion  dw ?
  4.         minorVersion  dw ?
  5.         numOfTables   dw ?
  6.         searchRange   dw ?
  7.         entrySelector dw ?
  8.         rangeShift    dw ?
  9. ends
  10.  
  11. ; Заголовок коллекции шрифтов
  12. struct TTC_HEADER
  13.         ttcTag        rb 4
  14.         majorVersion  dw ?
  15.         minorVersion  dw ?
  16.         numFonts      dd ?
  17.         offsetTable   dd ?
  18. ends
  19.  
  20. ; Запись в таблице
  21. struct TT_TABLE_DIRECTORY
  22.         tag         rb 4
  23.         checkSum    dd ?
  24.         tableOffset dd ?
  25.         tableLength dd ?
  26. ends
  27.  
  28. ; Заголовок таблицы наименований
  29. struct TT_NAME_TABLE_HEADER
  30.         formatSelector  dw ?
  31.         numberOfRecords dw ?
  32.         stringOffset    dw ?
  33. ends
  34.  
  35. ; Запись о наименовании
  36. struct TT_NAME_RECORD
  37.         platformID   dw ?
  38.         encodingID   dw ?
  39.         languageID   dw ?
  40.         nameID       dw ?
  41.         stringLength dw ?
  42.         stringOffset dw ?
  43. ends
Для определения названия шрифта надо найти в файле таблицу данных с тегом "name", а в ней найти интересующую нас строку.
  1.         ; Определить имя шрифта
  2.         mov     ebx,font_data
  3.         ; Проверить корректность заголовка шрифта
  4.         ; TTC
  5.         cmp     dword [ebx+TT_OFFSET_TABLE.majorVersion],'ttcf'
  6.         jne     @f
  7.         ; Смещение первого шрифта в коллекции
  8.         mov     eax,[ebx+TTC_HEADER.offsetTable]
  9.         bswap   eax
  10.         add     ebx,eax
  11. @@:
  12.         ; TTF
  13.         cmp     dword [ebx+TT_OFFSET_TABLE.majorVersion],0100h
  14.         je      @f
  15.         ; OTF
  16.         cmp     dword [ebx+TT_OFFSET_TABLE.majorVersion],'OTTO'
  17.         jne     .loc_done
  18. @@:
  19.         ; Количество записей
  20.         movzx   ecx,word [ebx+TT_OFFSET_TABLE.numOfTables]
  21.         xchg    cl,ch
  22.         ; Указатель на список таблиц
  23.         add     ebx,sizeof.TT_OFFSET_TABLE
  24. @@:
  25.         mov     eax,dword [ebx+TT_TABLE_DIRECTORY.tag]
  26.         or      eax,20202020h
  27.         cmp     eax,'name'
  28.         je      @f
  29.         add     ebx,sizeof.TT_TABLE_DIRECTORY
  30.         sub     ecx,1
  31.         jnz     @b
  32.         ; Таблица не найдена
  33.         jmp     .loc_done
  34. @@:
  35.         ; Указатель на таблицу и размер данных
  36.         mov     eax,dword [ebx+TT_TABLE_DIRECTORY.tableOffset]
  37.         bswap   eax
  38.  
  39.         ; Переместить указатель на заголовок таблицы наименований
  40.         mov     ebx,font_data
  41.         add     ebx,eax
  42.  
  43.         ; Количество записей
  44.         movzx   edx,word [ebx+TT_NAME_TABLE_HEADER.numberOfRecords]
  45.         xchg    dl,dh
  46.         movzx   eax,word [ebx+TT_NAME_TABLE_HEADER.stringOffset]
  47.         xchg    al,ah
  48.         lea     esi,[eax+ebx]
  49.  
  50.         ; Указатель на последнюю запись в списке
  51.         imul    eax,edx,sizeof.TT_NAME_RECORD
  52.         lea     ebx,[ebx+eax+sizeof.TT_NAME_TABLE_HEADER-sizeof.TT_NAME_RECORD]
  53. .loc_scan_name:
  54.         ; NameID должен быть Full name of the font - 1 (Big Endian)
  55.         cmp     word [ebx+TT_NAME_RECORD.nameID],0100h
  56.         jne     .loc_prev_name
  57.         ; Platform ID должен быть Microsoft - 3 (Big Endian)
  58.         cmp     word [ebx+TT_NAME_RECORD.platformID],0300h
  59.         jne     @f
  60.         ; с кодировкой Unicode - 1 или Symbol - 0
  61.         movzx   eax,word [ebx+TT_NAME_RECORD.encodingID]
  62.         cmp     ah,1
  63.         jbe     .loc_string
  64.         jmp     .loc_prev_name
  65. @@:
  66.         ; или Macintosh - 1 (Big Endian)
  67.         cmp     word [ebx+TT_NAME_RECORD.platformID],0100h
  68.         jne     .loc_prev_name
  69.         ; с кодировкой Roman - 0
  70.         cmp     word [ebx+TT_NAME_RECORD.encodingID],0
  71.         je      .loc_string
  72. .loc_prev_name:
  73.         sub     ebx,sizeof.TT_NAME_RECORD
  74.         dec     edx
  75.         jnz     .loc_scan_name
  76.         ; Название шрифта не найдено
  77.         jmp     .loc_done
  78.  
  79. .loc_string:
  80.         ; Длина строки
  81.         movzx   ecx,word [ebx+TT_NAME_RECORD.stringLength]
  82.         or      ecx,ecx
  83.         jz      .loc_prev_name
  84.         xchg    cl,ch
  85.  
  86.         ; Указатель на строку
  87.         movzx   eax,word [ebx+TT_NAME_RECORD.stringOffset]
  88.         xchg    al,ah
  89.         add     esi,eax
  90.  
  91.         mov     edi,font_name
  92.  
  93.         cmp     word [ebx+TT_NAME_RECORD.platformID],0300h
  94.         je      .loc_unicode_string
  95.  
  96. .loc_ascii_string:
  97.         ; Скопировать название шрифта в буфер
  98.         xor     eax,eax
  99. @@:
  100.         lodsb
  101.         stosw
  102.         loop    @b
  103.         jmp     .loc_terminate
  104.  
  105. .loc_unicode_string:
  106.         ; Скопировать название шрифта в буфер
  107.         shr     ecx,1
  108. @@:
  109.         lodsw
  110.         xchg    al,ah
  111.         stosw
  112.         loop    @b
  113.  
  114.         ; Завершающий ноль
  115. .loc_terminate:
  116.         xor     eax,eax
  117.         stosw
  118.  
  119. .loc_done:
  120.         ; Название шрифта найдено
  121.         cmp     word [font_name],0
  122.         je      .loc_error
  123.         ; Теперь в font_name находится название шрифта
В интернетах в качестве примера получения названия шрифта на протяжении многих лет с сайта на сайт копируют следующий код. Действительно, в большинстве случаев он вернет правильный результат. Но в этом коде есть серьезная ошибка, из-за которой на некоторых TTF-файлах будет получено неправильное название. Дело в том, что таблица "name" может содержать два набора строк с названиями шрифта, один из которых записан в ASCII, а второй - в юникоде. Если файл шрифта сформирован корректно, то эти названия совпадают. Однако, в процессе написания статьи я нашел в своей коллекции несколько шрифтов, у которых названия отличались. Еще бывают варианты файлов, в которых содержится только один набор строк, с такими файлами некорректно работает даже системный установщик шрифтов. Код по приведенной ссылке возвращает только ASCII-строку названия шрифта, тогда как функция CreateFont при вызове ориентируется на строку из юникодного варианта написания, если таковая существует. В таких случаях, соответственно, функция не находит загруженный шрифт. В моей реализации эта ошибка устранена, в качестве названия приоритетно берется именно юникодная строка, а если юникодного блока строчек в файле нет, то возвращается строка из блока ASCII.

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

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

Font.from.Memory.Demo.zip (79,494 bytes)


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

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

Комментарии

Отзывы посетителей сайта о статье
ManHunter (24.03.2019 в 11:33):
Поправил. В очередной раз огромное спасибо за помощь.
DRON (23.03.2019 в 18:00):
Ещё парочка неработающих шрифтов: https://www105.zippyshare.com/...4i/file.html
Baghdad Regular.ttf и Geezah Regular.ttf - код использует последнее MAC-имя, но у него не та кодировка.
Pretzel.ttf - MAC-имя находится за пределами шрифта (получаем Access Violation). Система и редакторы шрифтов при этом работают нормально.
RoscherkDL.ttf - часть строк в неверной кодировке, но система с этим справляется.

Большинство проблем решатся, если учесть, что таблица имен сортирована и нужные нам строки находятся ближе к концу. В качестве бонуса, нам не надо будет временно копировать в буфер ASCII строки, так как они в начале таблицы и до них дело дойдёт только при отсутствии Юникодовых строк.

Вариант с обратным сканированием и проверкой кодировок: https://pastebin.com/1aviNJDT
ManHunter (22.03.2019 в 16:39):
Ну вообще красота!
DRON (22.03.2019 в 15:25):
Ну тогда, для полноты, и TTC можно добавить:
        ; Проверить корректность заголовка шрифта
        ; TTC
        cmp     dword [ebx+TT_OFFSET_TABLE.majorVersion],66637474h
        jne     @f
        ; Смещение первого шрифта в коллекции
        ; https://docs.microsoft.com/en-...#collections
        mov     eax,[ebx+0ch]
        bswap   eax
        add     ebx,eax
@@:
        ; TTF
ManHunter (22.03.2019 в 14:52):
Переделал на юникод, добавил OTF и DEFAULT_CHARSET. Теперь вообще красота, все грузится. И на юникоде даже не пришлось проверять languageID, все и так работает. DRON, огромное спасибо! Теперь можно рисовать свой просмотрщик для шрифтов :)
DRON (22.03.2019 в 14:17):
Ещё можно добавить поддержку OTF шрифтов (они часто идут с тем же .TTF расширением):
struct TT_OFFSET_TABLE
        version       dd ?
        numOfTables   dw ?
        searchRange   dw ?
        entrySelector dw ?
        rangeShift    dw ?
ends
 
; Проверить корректность заголовка шрифта
        cmp     [ebx+TT_OFFSET_TABLE.version],00000100h
        je      @f
        cmp     [ebx+TT_OFFSET_TABLE.version],4F54544Fh
        jne     .loc_done
@@:           

и стоит заменить в CreateFont ANSI_CHARSET на DEFAULT_CHARSET иначе некоторые шрифты (в основном полноюникодовые) не рендерят даже латиницу.

https://www84.zippyshare.com/v...GI/file.html
В архиве есть OTF (IrinaC.ttf), юникод в имени (IRON.TTF, aggstock.ttf, &arabia.ttf, SLAINE.TTF), шрифт в котором нужно проверять languageID (Namu R.ttf), шрифты работающий только с DEFAULT_CHARSET (keyboard.ttf и Namu R.ttf).
ManHunter (22.03.2019 в 11:12):
Откорректировал код, чтобы он работал и с одиночными блоками ASCII-строк. Теперь корректно грузятся все шрифты из подборки DRONа. Архив обновлен, статья дополнена. Еще осталось проверить шрифты с юникодом в названии. DRON, поделишься? Я у себя что-то не нашел.
Алексей (22.03.2019 в 01:45):
В шрифтах есть один минус,а это его размер. Бывает, что размер шрифта в 50 раз больше самой программы :) Мне как то 500kb шрифт попался. 
Но опять же.. Подобрал отличный шрифт "Fixedsys" от терминала.. Размер 6kb под паком прям самое то.
А вот на счет названия шрифтов, дааааааааа... встречалась такая проблема, а то есть, получается как ? Название вроде правильное написал и при компиляции ошибки нет, даже шрифт внутри.. Так заводишь файл, а там стандарт висит. Винда дефолтный на отображение берет и всё.
ManHunter (21.03.2019 в 18:05):
Во, спасибо. Мельком глянул, у этих шрифтов нет юникодного блока названия. Из-за этого обламывается не только моя программа, а также плагин-гляделка шрифтов для Total Commander, и даже виндовый установщик шрифтов не показывает название. Про "TM" знаю, у меня код под A-функции.
DRON (21.03.2019 в 17:56):
Вот несколько не определяемых шрифтов: https://www43.zippyshare.com/v...6m/file.html
Я не стал включать множество шрифтов в имени которых присутствуют символы Юникода (обычно это символы TM=Trade Mark), так как с ними можно работать только через Wide API функции. В архиве только те шрифты для которых имя вообще не определяется.
Все шрифты я проверял простой заменой font.ttf, исходники не менялись, за исключением временного добавления строки "invoke lstrcpy,font_name,'Правильное имя'", чтобы быть уверенным что шрифт вообще нормально загружается и отображается. Проверялось на Win7-x64.
ManHunter (21.03.2019 в 01:27):
Земляков обижать не буду. Заменил.
aarn (20.03.2019 в 23:41):
ManHunter !
Укажите пожалуста атора картинки КрошкиШи, которая в начале Вашего поста. 
Атор, Леся Гусева, очень не приветствует использование её рисунков без согласования с ней.
Она ведь пермячка, как и Вы (Вы когда-то ведь были PermCrackLab)
ManHunter (20.03.2019 в 10:48):
DRON, так все в порядке, именно так и делается. Проверяется NameID на соответствие Font Family, для CreateFont хватает именно этой строки. Я при написании статьи проверил точно не менее сотни разных шрифтов, юникодного Font Family для любого из них было достаточно. Шрифты, о которых я упомянул, как раз были конвертированы из каких-то других, и там действительно название в ASCII не совпадало с названием в UNICODE. Если есть какой-то шрифт, не подходящий под условие, то выложи куда-нибудь, я с удовольствием посмотрю. При необходимости сразу подкорректирую код.

CreateFont загружает шрифт по Font Family, а в параметрах выбирается кодировка, всякие болды с италиками, язык и прочая хрень. На основании этих данных система сама разберется, какой из загруженных шрифтов этого семейства задействовать.
DRON (20.03.2019 в 01:02):
Я когда-то писал подобное и там всё намного сложнее. Вот кусок кода из утекших исходников винды: https://pastebin.com/Vw9er2NL
Как минимум, нужно учитывать различные типы строк (FaceName, Family, SubFamily итд), а также соответствие языка строки языку системы.

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

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

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