D:\sideБлогПроламываем шрифты\1

⚠️ Обращайте внимание на даты.
Этот блог больше не ведётся с 17 января 2023, и на тот момент с написания этой страницы (22.04.2018) прошло 4 года.

Те, кто наблюдает за моей деятельностью, могли заметить, что я когда-то участвовал в переводе FTL: Faster Than Light на русский язык. Давно дело было, ещё в то время, когда первое крупное дополнение только вышло, но предыдущий русификатор (от ZoG) уже был безнадёжно им сломан. Впрочем, тогда единственное, что стояло у нас на пути, это обработка больших объёмов переводимого текста. Но в начале года, близ релиза Into the Breach от той же игровой студии, FTL снова обновился, внезапно и сильно. И нашу жизнь здорово осложнил тот факт, что для добавления в игру кириллицы простым редактированием TTF-файлов мы уже не обойдёмся: формат шрифтов был обновлён. Бегло осмотрев парочку новых файлов, я не нашёл ни единой зацепки, позволявшей узнать в них какой-либо из известных мне форматов. Не могу сказать, что я эксперт по форматам шрифтов, но у файлов были характерные черты, а поисковики по всея интернету не могли мне о них рассказать абсолютно ничего. Но в конечном итоге всё получилось.

Глава 1: отчаяние

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

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

Открыв hex-редактор, я на некоторое время растерялся. Я просто не знал, что искать. Но в самом начале файла меня привлекла вполне себе человекочитаемые первые 4 байта: ASCII-буквы FONT. После этих байт, правда, шла совершеннейшая каша. Но после этой каши нашлось не менее человекочитаемое TEX, ещё немножко каши и некоторое количество “космоса” из нулей. В зашифрованном файле вряд ли так далеко от конца файла были бы такие “поля” из одинаковых значений, зашифрованная информация типично выглядит случайной.

Хорошо, шифрования явно нет. Но что такое TEX?

Глава 2: угадайка

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

16 байт. Подозрительно круглое число, с программистской точки зрения: степень двойки. И иного значения у этого числа быть просто не может: это размер одной записи о символе. Однако неизвестно, где каждая запись начинается и заканчивается, а потому установить содержимое каждой из них… затруднительно.

Попробуем зайти с другой стороны: попробуем выяснить, что там должно быть. Давненько мы не смотрели на TEX! А к этой пометке неожиданно появились новые вопросы, поскольку при просмотре файла 16-байтной лентой она оказалась аккурат в начале строки. Не совпадение, определённо.

\(16416\), \(65568\), \(32800\)… размеры секций TEX казались подозрительными, но я долго не мог понять, почему. Особенно \(65568\), очень уж похоже на \(65536\), одно из моих любимых чисел, \(2^{16}\). По-хорошему, это \(2^{16} + 32\). Да и остальные не хуже: \(2^{15} + 32 = 32800\); \(2^{14} + 32 = 16416\)… То есть, если от начала TEX отступить 32 байта “каши” (непонятных данных) вперёд, остаются данные длины аккурат с одну из степеней двойки. И каши после них уже нет, поначалу одни нули. Тоже непохоже на совпадение.

Глава 3: холст и масло

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

И тут меня осенило. TEX означает texture, текстура! Их размеры принято делать степенями двойки из соображений производительности! Я совершенно зря ищу отдельные картинки с символами, их нет — вместо них есть одна здоровенная картинка, на отдельные фрагменты которой нанесены рисунки символов!

Вопиюще правдоподобно, но как проверить? Я ведь не знаю размеров этих картинок. Хотя… нужно ли мне это?

Площадь каждой картинки является степенью двойки: в разложении таких чисел на простые множители нет ничего кроме двоек. А площадь, на секунду, произведение ширины и высоты, что означает, что они тоже — степени двоек. Перебор даже картинки объёмом \(2^{16}\) виделся очень недолгим. Отдалённо припоминая, что для видеокарточек предпочитают квадратные текстуры, я решил начать с размера \(256 \times 256\).

Чтобы иметь нечто более осязаемое, я взялся за свой основной рабочий язык, Ruby, и взял первую попавшуюся библиотеку для сборки изображений популярного беспотерьного формата PNG: ею оказалась ChunkyPNG. Результат появился практически немедленно, но буквы оказались повёрнуты набок и отзеркалены. Подумаешь, перепутал оси, с кем не бывает.

Вдохновлённый успехом, я натравил программку на все файлы, строя из каждого PNG-картинку шириной 256 точек. Почти получилось. “Поехавшей” оказалась всего одна картинка, самого мелкого шрифта, текстура у которой оказалась ширины 128.


Собственно, это была основная часть “детективной работы”. Далее интересные моменты тоже были, но касающиеся уже больше исполнения и экспериментов, чем исследования; и они представляют уже заметно меньший интерес. Но и там не обошлось без курьёзов. Об этом во второй части.

Интермиссия: выкинуть в окно

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

До этого момента моим основным подельником был пингвин Linux, но у коллеги-мододела была Windows-система. Чтобы он тоже смог воспользоваться моими трудами, их следовало проверить и на окнах. Но у окон обнаружились свои… особенности. Я полагал, что указав для чтения и записи кодировку Encoding::BINARY, я получу бинарные строчки из файлов “как есть”, без искажений. Я ошибался.

Главным образом, для всего ввода и вывода с участием файлов пришлось отдельно добавить в режимы открытия буковку b, потому что в противном случае вместо одиночного байта с кодом 10 (\n) считывалось два байта, 13 и 10 (\r\n), и в результате мои программы выдавали довольно-таки странные вещи.

Примерно в это же время пришлось подраться с git из-за того, что он запомнил две разных папки с одинаковыми названиями с точностью до букв, но не их регистра. Git к такому привычен. А вот Windows со своей регистронезависимой файловой системой NTFS переживал культурный шок всё то время, что я пытался вышвырнуть лишнюю папку из репозитория.

Но это так, мелочи. Важные, но мелочи. Мой ящик скальпелей и гаечных ключей (Ruby, стандартная библиотека и bitstruct) и основной инструмент досмотра (ChunkyPNG) были вполне работоспособны, и продолжить разработку на Windows можно было относительно безболезненно.