Pull to refresh

Разоблачение алгоритмов растеризации шрифтов (2/2)

Reading time 14 min
Views 10K
Original author: Maxim Shemanarev
(вторая часть перевода статьи Разоблачение алгоритмов растеризации шрифтов)

Linux


Наследуя худшее


Windows растеризует шрифты плохо, Linux ещё хуже. Во всех Linux-системах, которые я видел, используется FreeType [10] Дэвида Тёрнера, Роберта Вильгельма и Вернера Лемберга. Это отличная библиотека, но способ её использования, к сожалению, нельзя назвать удачным. Типичный скриншот Linux выглядит так:



Вот полный скриншот:
ссылка

Сразу заметна проблема — чёрные пятна в скругленных углах, образовавшиеся в результате сглаживания. Вцелом, можно сказать, что косые штрихи выглядят тяжелее чем вертикальные, что в регультате производит впечатление «грязи». Вы можете возразить, что FreeType и Linux могли бы использовать схожую с ClearType субпиксельную растеризацию, но по мне это не даёт заметных преимуществ.



Посмотрите на «W», «v» и «y» — проблема в сущности та же, символы выглядят грязно. Можно слегка улучшить ситуацию в углах, используя корекцию гаммы в процессе растеризации, но это всё равно не позволит добиться идеального отображения.

Коррекция гаммы


Коррекция гаммы работает так:



Как вы могли заметить, скругленные углы со сглаживанием смотрятся значительно лучше с гаммой 2.0. Коррекция гаммы — отдельная нетривиальная тема, и, если вам интересно, вы можете найти исчерпывающую информацию в «Gamma FAQ» Чарльза Пойнтона [6].

В нашем случае речь не идет о кривых «исходный сигнал — результат» в электронных цепях, скорее, о специфике человеческого зрения. Зрительная реакция примерно пропорциональна квадратному корню физической светимости. Другими словами, если у нас есть два белых пикселя на черном фоне, и один из них излучает в два раза больше фотонов, чем второй, это не значит, что он будет выглядеть в два раза ярче. На самом деле, где-то в 1,4 раза. Вы можете легко убедиться в этом:



Справа — два пикселя, и мы можем с уверенностью сказать, что они излучают в два раза больше фотонов в секунду, чем пиксель слева. Тем не менее, они не выглядят в два раза ярче. Четыре пикселя дадут примерно в два раза большую яркость, но никак не два.

Опустив лишние объяснения, мы можем сказать, что есть два основных цветовых пространства RGB: субъективно равномерное, которое называется sRGB, и физически равномерное. В последнем значение пропорционально физической светимости, в отличие от sRGB, в котором значение пропорционально субъективной светимости. Обычно физически равномерное цветовое пространство называют просто «линейный RGB». При использовании сглаживания сведение цветов должно производиться в линейном пространстве, но перед выводом на экран необходимо привести цвета к sRGB. На практике этот шаг часто игнорируется, и сглаживание выполняется непосредственно в sRGB. Во многих случаях это дает приемлимый результат, но не для растеризации текста, что можно продемонстрировать прямо в Microsoft Word. Штука в том, что они используют некий трюк для выделения текста, что-то типа тривиальной инверсии, вместо перерисовки с нуля. С обычным, черно-белым сглаживанием выделенный текст смотрится грязно. С ClearType он приобретает цветную кайму:



Так что мы можем сделать вывод, что в Windows выполняется корректная коррекция гаммы (но не для выделенного текста), в Linux же её обычно игнорируют. FreeType может легко применить нужное преобразование гаммы к черно-белой маске сглаживания, которую генерирует растеризатор. Но это будет работать так же, как в Windows: инверсия цветов даст инверсию гаммы. На практике это безполезно, так как гамма должна применяться по отдельности к каждому цветовому компоненту до смешивания (что на практике эквивалентно работе в линейном RGB). Поэтому коррекция гаммы для черно-белой маски поможет, только если вы выводите черный текст на белом фоне. В этом случае вы можете использовать для коррекции значение в районе 2. Но если вы выводите белый текст на черном фоне, вам нужно инвертировать значение гаммы, то есть, использовать что-то около 0,5. Проблема в том, что вы не знаете точного цвета текста и фона, текст также может быть выведен поверх градиента или изображения. Так что «черно-белая» коррекция гаммы не сработает, а «полноцветная» коррекция гаммы может быть дорогостоящей и сложной в реализации. Проблема в том, что для линейного RGB нужно больше 8 битов на канал, иначе вы неизбежно получите потери цвета. Для текста это может быть допустимым, но вы не можете требовать этого для всего рабочего стола! А работа в линейном RGB, используя по 16 бит на канал — до сих пор непозволительная роскошь.

Гамма не работает


На самом деле, ситуация ещё хуже. Вы можете применить коррекцию гаммы со значением, равным 2 к скриншоту с Linux в том же Irfanview (Image->Enhance colors...) и посмотреть на текст. Пожалуйста, постарайтесь не обращать внимание на то, что пиктограммы выглядят пересвеченно, сконцентрируйтесь на тексте.



Вам нравится? Мне всё ещё нет. Когда я работал над растеризацией текста в AGG, я думал, что правильная коррекция гаммы может решить все проблемы. Ничего подобного! Независимо от того, насколько хорошо она работает, некоторые элементы выглядят толще, а некоторые — тоньше, чем вертикальные и горизонтальные штрихи. Это сильно заметно на шрифтах без засечек, и особенно — когда штрихи жёстко выровнены по пикселям. Проблема в том, что хинтинг TrueType был специально разработан для обычного черно-белого растеризатора без сглаживания! Использование любого сглаживания формально некорректно, а большинство Linux систем делают именно это. Изображение ниже — результат растеризации со сглаживанием с применением как FreeType, так и GetGlyphOutline().



Текст выглядит паршиво и это очень похоже на то, что мы в большинстве случаев наблюдаем в Linux. Здесь не поможет никакая коррекция гаммы. Для примера, лучший результат у меня получился со значением гаммы равным 1,5. Всё ещё выглядит плохо:



По ходу дела вы должны были заметить, что, начиная с определенного размера текст начинает казаться тяжелым. Это именно то, что происходит в Windows. Если вы выключите ClearType, это будет очевидно (размер текста не сохраняется в точности).



В общем, идею вы поняли. Чтобы она стала более очевидной, мы можем увеличить векторное изображение, которое возвращает функция GetGlyphOutline() из Win32 API, и посмотерть, что будет происходить.



Вот так работает патентованный агрессивный хинтинг для номинального размера в 13 пикселей. Вот почему штрихи в «k» выглядят такими тонкими, почти невидимыми. В курсиве Times New Roman всё ещё хуже: полностью исчезает косой штрих в «z». Искажения не влияют на обычный растеризатор без сглаживания, но тот, что использует FreeType, чувствителен к этим вещам. Он непосредственно рассчитывает степень покрытия пикселей, так что он честно получает нулевое покрытие для косого штриха «z». То есть выясняется, что нет никакого смысла интерпретировать байткод TrueType для хинтинга (не говоря уже о том, что вам придется покупать лицензию). Сглаживание это хорошо, но его не стоит применять ради самого себя. В любом случае, я бы предпочел текст без сглаживания, если оно используется в паре с неадекватным хинтингом.

Автохинтер FreeType


В FreeType версии 2 Дэвид Тёрнер представил механизм автохинтинга. Он работает вполне неплохо, но, тем не менее, непосредственное его применение даёт результат, далёкий от идеального. Посмотрите на результат растеризации гарнитуры Verdana с гаммой 1,5:



Сравните с примером без хинтинга:



Версия без хинтинга определенно выглядит более аккуратной, но и более размытой. Есть три основные различия:

  1. Автохинтинг всё ещё даёт ошибки в скругленных элементах небольшого размера (та же визуальная разница в толщине между вертикальными и косыми штрихами).
  2. Иногда автохинтинг приводит к некорректному кернингу, как в «og» в слове «Dog» (в этом примере использовалась кернинговая таблица из шрифта).
  3. Автохинтинг приводит к той же проблеме накопления ошибки на протяжении строки текста, в результате чего правый край текста приобретает «зазубрины».

Автохинтер работает лучше с более сложными шрифтами, такими как Times New Roman, но всё равно встречаются те же проблемы позиционирования.

Что же делать?


Заглядывая вперед, я покажу вам ещё один пример.



Всё-таки возможно найти приемлимое решение! Но для начала вам нужно согласиться, что нет способа использовать хинтинг любого рода и при этом сохранять корректное расположение текста на любом масштабе. Только текст без хинтинга, с его естественной размытостью. Тем не менее, мы можем улучшить растеризацию, хотя нам и придется пожертвовать чем-то не очень важным. А именно, мы можем позволить себе небольшую неточность в вертикальном позиционировании и высоте текста. Кроме всего прочего, хинтинг TrueType работает так же: строки текста высотой, скажем, 12 и 13 пикселей будут на экране иметь одну и ту же высоту, хотя выглядеть будут по-разному.



Короче говоря, для приятного глазу текста и при этом сохранения аккуратного горизонтального позиционирования нам нужно следующее:

  1. Использовать горизонтальное субпиксельное RGB-сглаживание на мониторах LCD.
  2. Использовать только вертикальный хинтинг и полностью отказаться от горизонтального.
  3. Использовать точные значения смещения глифов, вычисленные на высоком разрешении для глифов без хинтинга.
  4. Использовать точные значения высокого разрешения из таблицы кернинга.

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

Вы можете легко добиться приемлимого результата с FreeType и её автохинтером. Это значит, что вам не нужно беспокоиться о лицензировании патентованного хинтинга TrueType. То же самое можно проделать, используя функцию GetGlyphOutline() из Win32 API. Это сложнее, но всё равно возможно.

Субпиксельная растеризация в RGB


Вы можете найти исчерпывающее руководство по использованию субпиксельной растеризации в RGB на страницах Стива Гибсона, «Технология субпиксельной растеризации» [2]. Я также пробовал использовать эту технологию с 64-уровневыми растровыми изображениями со сглаживанием, которые может генерировать GetGlyphOutline(): Максим Шиманарев, «Как устроен ClearType в Windows Longhorn» [3] (UPD: ссылка протухла, но можно прочитать здесь, здесь или здесь). Вы можете скачать демонстрационную программу для Windows со всеми исходниками по этой ссылке:
antigrain.com/stuff/lcd_font.zip

Кроме того, я написал простой, «на коленке за вечер», растеризатор для AGG. Его можно найти в демонстрационных примерах, которые последуют ниже. На данный момент код небезопасный и довольно медленный. Это нормально для демонстрационных программ, но неприемлимо в реальном проекте, в первую очередь из-за того, что он использует временный буфер для не более 2048 пикселов в стеке.

В самом простом случае, всё что нам нужно, это значения прозрачности для каждого цветового канала. В данном файле, agg_pixfmt_rgb24_lcd.h. Я также использовал дополнительное размытие, которое описывает Стив Гибсон. Оно выполняется на ходу, хотя может быть выполнено заранее с использованием некого механизма кэширования. В этом случае оно будет работать значительно быстрее, как минимум, не медленнее, чем классическое альфа-смешивание.

Для отладки смешивания по каналам я использовал программу ZoomIn [9] Брайана Фрайзена. Я добавил «декодирование» троек цветов на всех масштабах, кратных трём. Вы можете скачать исполняемый файл здесь: antigrain.com/stuff/ZoomInLcd.zip (в 2005м я потерял модифицированные исходники. В любом случае, эти модификации нетрудно выполнить самостоятельно). Вы можете сравнить увеличенные результаты обычной черно-белой и субпиксельной RGB растеризации:


Черно-белая растеризация


Субпиксельная RGB растеризация

Другие нюансы


Чтобы сохранить вертикальный хинтинг, но избавиться от горизонтального, мы просто обманем хинтер: растянем символы по горизонтали так, чтобы хинтер был вынужден работать с высокой точностью. Проблема в том, что шрифтовой движок AGG для FreeType использует неточные значения смещения, учитывая хинтинг. Технически, хинтер должен вычислять точные значения смещения для сильно растянутых глифов, но по непонятной причине он этого не делает. Мне пришлось модифицировать его, чтобы использовать исходные, «нехинтованные» смещения. Измененная версия также включена в демонстрационные программы. После того как кривая глифа получена, мы используем аффинное преобразование, чтобы сжать её обратно. В самом простом случае это всё что нам нужно. Таблица кернинга содержит достаточно точные значения.

Так что я хочу обратиться к Дэвиду Тёрнеру: может быть, есть смысл добавить в его автохинтер опцию, которая позволяла бы выполнять хинтинг только по оси Y, игнорируя хинтинг по оси X? Или можно сделать отдельный 1-D хинтер, значительно проще существующего. Как вы увидите, текст с субпиксельной RGB растеризацией смотрится очень похоже на Adobe Acrobat Reader, и, в любом случае, гораздо лучше, чем в любой современной Linux системе (статья написана в 2007 году — прим. перев.). Я верю, что это поможет продвижению и увеличению популярности систем на основе Linux.

Использование Windows API существенно сложнее. Функция GetGlyphOutline() возвращает значение смещения в целочисленных пикселях, что для нас слишком грубо. Растягивание не спасает. Есть ещё функции вроде GetCharABCWidthsFloat(), но они бесполезны, так как они рассчитывают значения для хинтованных глифов и несмотря на тот факт, что они содержат числа с плавающей точкой, всё равно, на самом деле, это целые числа. Так что я не нашел простого способа получить точные смещения. В результате мне пришлось использовать два шрифта одновременно, один высотой в 1024 пикселя, а другой нужного нам размера, с хинтингом и растянутой аффинной матрицей. Я допускаю, что мог что-то пропустить, но у меня нет мыслей о том, как это можно реализовать более корректно. Возможно, в Microsoft Word они используют какие-то недокументированные функции, что является абсолютно нечестным с точки зрения конкуренции. Конечно, я не могу быть в этом полностью уверен, но ситуация заставляет меня думать, что Microsoft намеренно не предоставляет достаточного хорошего API для разработки средств работы с документами WYSIWYG. Это типичная политика монополиста, которая в результате приводит к торможению технического прогресса.

На самом деле, всё ещё хуже. Патентованный хинтер не работает с «растянутой» матрицей! Как минимум, я не нашел ни один коэффициент масштабирования, который бы корректно обрабатывал глифы. Работал корректно только масштаб 1:1, но в результате я получил те же проблемы, которые вынудили меня использовать чёрно-белый растеризатор без сглаживания:



Выглядит жутко, не правда ли? Любое масштабирование приводило к сильно испорченным глифам. Вот, например, курсив Times New Roman (16-кратное растягивание по горизонтали):



Или даже так. Arial (100-кратное растягивание по горизонтали) — забавные подтёки, да? Но читать невозможно:



Думаю, нет смысла говорить, что автохинтер FreeType работает корректно при любом растягивании.

Выглядит так, будто бы Microsoft API — просто набор нездоровых случайных решений «на коленке» без всякой инженерной культуры и глобальной идеи, стоящей за всем этим. Как правило, вы можете использовать ПО Microsoft только одним жёстко предопределенным образом. Шаг вправо, шаг влево — и всё пропало. Быть может, это и хорошо для их бизнеса, но, как минимум, нечестно. Такая политика нарушает условия равноправной конкуренции и в результате замедляет общий прогресс на рынке. Антимонопольному комитету следовало бы обратить внимание на эту ситуацию, вместо смешных требований убрать из системы Media Player или Internet Explorer.

В конечном итоге я выяснил, что значение «16» — меньшее зло, оно подходит для большинства случаев, но всё ещё не срабатывает для курсива Times New Roman.

Демонстрационная программа


Вот она, программа для Windows, использующая TrueType:
www.antigrain.com/research/font_rasterization/truetype_test_02_ft.zip

А вот версия, которая использует Windows API:
www.antigrain.com/research/font_rasterization/truetype_test_02_win.zip

Версия под FreeType требует наличия в каталоге программы следующих файлов: arial.ttf, ariali.ttf, georgia.ttf, georgiai.ttf, tahoma.ttf, times.ttf, timesi.ttf, verdana.ttf, verdanai.ttf. Вы можете найти их в папке %WINDIR%\Fonts.

Если вы захотите скомпилировать её, скачайте AGG версии 2.4 или 2.5 и распакуйте файлы куда-нибудь вроде agg-2.4\research\win32\trutype_lcd\*.*. Для версии под FreeType вам также понадобится самостоятельно собрать FreeType, и, возможно, изменить настройки проекта.

Программа также может быть собрана для Linux/X11 или другой системы, если вы напишите соответствующий makefile, подобный тем, которые используются в примерах AGG.

Текст в версиях под FreeType и под WinAPI выглядит по-разному из-за разных алгоритмов хинтинга.



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

Слайдер «Font Scale» позволяет плавно менять размер шрифта. Как вы можете заметить, при включенном хинтинге линии привязаны к пикселям, но при этом ширина текста продолжает изменяться плавно. Вы сможете лучше разглядеть этот эффект, изменяя интервал. Без хинтинга разположение текста идеально сохраняется на любом масштабе, но текст выглядит размытым. Вертикальная привязка линий — наиболее разумный компромисс между резкостью и точностью расположения текста. Я сам в шоке от того, насколько вертикальный хинтинг улучшает качество и при этом сохраняет форму символов.

Слайдеры интервала, ширины и «имитации курсива», я думаю, не нуждаются в комментариях. Для людей, знакомых с компьютерной графикой, очевидно, что это обычные аффинные преобразования. Я бы хотел отметить только один факт: в режимах «черно-белый» и «субпиксельная RGB растеризация» слайдер «имитация курсива» работает немного по-разному. Это происходит потому, что я был слишком ленив, чтобы обрабатывать его значения корректно, через арктангенсы. В любом случае, это несущественно.

Функция, которой я особенно горжусь, это «имитация полужирного». Она работает так:



Есть ещё один простой трюк. В составе AGG есть утилита conv_contour, которая позволяет вам расчитать для данного многоугольника эквидистантный. Но непосредственное её использование дает слишком размытый результат, а также заметно изменяет форму знаков (хотя это может быть полезно для эффектов свечения и тени):



Размытия избежать легко. Мы растягиваем глифы по вертикали, скажем, в 100 или 1000 раз, вычисляем эквидистантный многоугольник и сжимаем его обратно. Так что в результате координаты по оси Y почти не меняются и текст остаётся чётким. В демонстрационной программе есть класс «faux_weight». Опять же, потрясающе, сколько возможностей даёт свободное горизонтальное масштабирование. И не менее потрясающе, насколько привязка к пикселям по вертикали улучшает визуальный результат.

Ещё один пример (я люблю эту свободу):



Это всё та же гарнитура Georgia, но только программно преобразованная. Вполне читаемая, чёткая и гладкая одновременно (да, я согласен, ей бы не помешал ручной кернинг).

Или тоже самое для гарнитуры Tahoma:



Слайдер регулировки гаммы управляет корекцией гаммы (она выполняется отдельно для каждого канала). Теоретически, следует применять «непосредственную гамму» к исходным цветам, а затем, после отрисовки сцены, применять «инвертированную гамму». Но, поскольку текст в этих примерах всегда белый или черный, в первой операции нет никакого смысла.

Слайдер «первичный вес» управляет распределением энергии также, как это описано у Стива Гибсона: www.grc.com/freeandclear.htm. Достаточно хорошо управлять только первичным весом, а остальные рассчитывать соответственно. Увеличивая первичный вес вы можете сделать текст чётче, но при этом появляется цветной контур вокруг знаков. Имеет смысл использовать значения до 0,5, при больших значениях цветовое «свечение» становится слишком заметным. Как по мне, Windows ClearType тоже даёт слишком заметное цветное «свечение».

Такая растеризация может работать быстро


Демонстрационная программа довольно медленная. Отчасти потому что векторные операции производятся на лету, но в первую очередь из-за функции WinAPI GetGlyphOutline(), которая сама по себе ужасно медленная. С другой стороны, подобная растеризация может быть не менее быстрой, чем любая аппаратно ускоренная растеризация текста. Но для начала вы должны согласиться с тем, что акселерация растеризации произвольно изменяемого текста с сохранением хинтинга, корректной разметки и чёткого субпиксельного качества — в принципе непростая задача. Под произвольными трансформациями я имею в виду действительно произвольные, включая перспективу и любые нелинейные преобразования.

Большую же часть времени нам приходится иметь дело с горизонтальным текстом, даже при использовании восточно-азиатских языков. К тому же, большую часть времени глифы используют один и тот же номинальный размер. Из этого следует, что здесь пригодился бы механизм кэширования. Субпиксельная черно-белая маска для трёх каналов RGB требует в три раза больше памяти, но в то же время дает точность позиционирования текста до 1/3 пикселя. В большинстве случаев это работает достаточно хорошо. Разве что для теоретической «роскошной» растеризации вы можете использовать две черно-белые маски на глиф, получая точность в 1/6 пикселя. Даже программное альфа-смешивание работает достаточно быстро — порядка 2-4 мс на глиф на современных процессорах Intel или PPC. С использованием GPU это может происходить ещё быстрее, если загрузить соответствующие текстуры. Единственная проблема — GPU должен позволять альфа-смешивание по каналам, что, насколько я знаю, представляется возможным. Как минимум, Дэвид Браун упоминает об этом в своей презентации [8]. Но я не смог найти больше информации по этой теме (как получить 6-канальный вывод из пиксельного шейдера) и я был бы благодарен, если бы вы предложили мне какие-нибудь ссылки по этой теме.

Ссылки


  1. Joel Spolsky, Font smoothing, anti-aliasing, and sub-pixel rendering.
    www.joelonsoftware.com/items/2007/06/12.html
  2. Steve Gibson, Sub-Pixel Font Rendering Technology.
    www.grc.com/cleartype.htm
  3. Maxim Shemanarev, Inside ClearType in Windows Longhorn.
    www.byte.com/documents/s=9553/byt1113241694002/0411_shemanarev.html
    (requires online registration.)
  4. FontFocus white paper, artofcode.com/fontfocus
  5. Jeff Atwood, Font Rendering: Respecting the Pixel Grid.
    www.codinghorror.com/blog/archives/000885.html
  6. Charles Poynton, Frequently-Asked Questions about Gamma.
    www.poynton.com/GammaFAQ.html
  7. Dave Shea, A Subpixel Safari.
    mezzoblue.com/archives/2007/06/12/a_subpixel_s
  8. David Brown, Avalon Text. A PowerPoint presentation.
    download.microsoft.com/download/1/8/f/18f8cee2-0b64-41f2-893d-a6f2295b40c8/TW04007_WINHEC2004.ppt
  9. Brian Friesen, ZoomIn Program.
    www.csc.calpoly.edu/~bfriesen/software/zoomin.shtml
  10. David Turner and the others, FreeType font Library.
    freetype.org
  11. Jim Mathies, XP Style DPI Scaling.
    www.mathies.com/weblog/?p=908
  12. Long Zheng, Windows Vista DPI scaling: my Vista is bigger than your Vista
    www.istartedsomething.com/20061211/vista-dpi-scaling

PS: Первая часть статьи в вебархиве (с картинками). Вторая часть статьи в вебархиве (с картинками).

PPS: Несколько любопытных ссылок для интересующихся темой экранной типографики:
Кегль шрифта: не всё так просто
Шрифты на бюджетном мониторе в Windows 8
Высокие значения DPI в ОС Windows
В защиту программистов
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+118
Comments 49
Comments Comments 49

Articles