Контур текста в GDI с антиалиасом

GDI Text Contour

Контур текста довольно полезная штука. Не только в праздничном оформлении, но и во вполне деловой практике. Оконтуренный текст достаточно просто получить в GDI+ и, прямо скажем, совсем не просто в D2D. Однако, как быть в старом добром GDI?

GDI Path

В старом добром GDI есть такая штука, как траектория, Path. В контексте устройства может быть только одна траектория, и это не объектная вещь, как в GDI+. Поэтому начало траектории в контексте DC знаменует функция именно такого вида:

и завершает траекторию:

Между BeginPath и EndPath могут находиться функции рисования, список которых можно узнать сходив по ссылке.

Нарисовать получившуюся траекторию текущим пером можно функцией:

Если требуется только залить текущей кистью, то используется функция:

Если требуется нарисовать контур и залить, то используется такое комбо:

Таким образом, чтобы нарисовать контур текста, необходимо сделать следующее:

И, если честно, то получилось так себе:

Рис.1. GDI Path

Потому что зубцы и ступеньки:

Рис.2. GDI Path — не совсем то, что хотелось бы видеть

GDI+ Path

Процесс рисования контура в GDI+ и замысловатей, и проще одновременно:

Вначале происходит инициализация всех нужных классов.

Затем происходит отрисовка текста в траектории gpPath:

Затем мы рисуем траекторию либо контуром, либо контур и заливка. Алгоритм, по сути, тот же самый, что и для GDI Path.

Отдельно хочется акцентировать на создании шрифта:

Это очень простая строка, и почему так происходит можно посмотреть тут.

И далее мы просто вытаскиваем из шрифта все нужные данные:

Также можно посмотреть нюансы TGPFontFamily.

Результат прекрасен, именно то, что хотелось видеть. Да, я топлю за GDI+ )))

Рис.3. GDI+ Path — это то, что хотелось видеть

Время отрисовки правда больше в 3 раза, чем в GDI, но ведь красота требует жертв, не правда ли?

Напоминаю, это всё таки GDI+ со всем своим неисчерпаемым ресурсом изобразительных средств. Например, если вместо скучного одноцветного пера мы создадим перо на изображении, станет повеселее:

Естественно, всё что создаём, в конце надо освободить.

Рис.4. GDI+ Path перо на картинке

В процедуру передаётся координата точки отрисовки для текста. Она одинакова для всех процедур, используемых в этом проекте. И считается следующим образом:

Так вот, в GDI+ свои понятия о ширине, высоте и выводе текста на экран. Поэтому будет наблюдаться некоторое смещение. Если работаем в GDI+, то при расчётах координат необходимо придерживаться понятий GDI+.

GDI Text

Траектория в GDI — хороший инструмент. Но в силу своей абсолютной не-эстетичности, и нашего заскорузлого перфекционизма, для отрисовки контура текста нам категорически не подходит. Зубцы.. ступеньки.. Скорость — да, впечатляет. Остальное — отстой.

Кто выбрал для себя комфортный путь GDI+, дальше может не читать.

Кто упёрт и верит в тернистый и извилистый GDI, двигаемся дальше.

Никакого другого способа нарисовать красивый контур текста, используя только GDI, кроме как нарисовать тот же самый текст, но со смещением по разные стороны от местоположения текста — нет.

Непрозрачный контур текста

Сказано-сделано.

Рис.5. GDI Text.

А неплохо.

Единственная проблема, такой текст не может быть прозрачным. Потому что рамка на самом деле — это один и тот же текст, нарисованный со смещением со всех сторон от конечного местоположения текста.

Грубо это можно представить так:

Рис.6. GDI Text. Как рисуется контур

А потом поверх «шлёпается» текст с заданным цветом. Происходит это тут:

Рис.7. GDI Text. Нарисовать текст поверх

Если вот эта вся мешанина под текстом была бы одного цвета, то всё выглядело бы так:

Рис.8. GDI Text. Жирный контур непрозрачного текста

Теперь давайте решим проблему прозрачности.

Прозрачный контур текста. Спрайт

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

Перенести часть битмапа на другой можно кучей разных способов, которые по большому счёту, делают одно и то же. Собирают в одно целое разные части с указанием битовой операции, которую нужно применить к пикселям того или иного изображения. Я буду использовать BitBlt и по минимуму MaskBlt. В итоге, мы всё равно откажемся от этой концепции.

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

Вторая причина — со спрайтами работа именно такая. Это не пригодится только в случае рисования текста. Если будет когда-нибудь статья про спрайты, то можно будет сюда сослаться. А в самой статье сделать несколько по другому. Для полноты картины будет весьма полезно.

Шаг1. Сохранить фон

Итак, перед тем, как начать рисовать контур, запоминаем фон.

Рис.9. Сохранили фон под текстом перед будущей рисовкой

Шаг 2. Рисование текста

Затем рисуем контур, а вернее этот дикий и пока не оптимизированный массив текста.

Рис.10. GDI Text. Заготовка под контур

Шаг 3. Маска

Затем готовим маску, рисуем белые буквы на чёрном фоне:

Рис.11. Маска

Шаг 4. Подготовка прозрачной части фона

После этого готовим битмап, содержащий часть, которая должна стать прозрачной в итоговом изображении:

Рис.12. Прозрачная часть текста, та что внутри контура

Шаг 5. Обратная маска

После этого рисуем обратную маску, где фон белый, а буквы чёрные:

Рис.13. Маска наоборот

Шаг 6. Перенос маски на итоговое изображение

Далее переносим черные буквы на прозрачную часть в итоговое изображение:

Рис.14. Подготовленное поле «прозрачности»

На рисунке 10 помните была серая заготовка под контур. Вот сейчас видно, что от неё останется.

Шаг 7. Копирование прозрачной части фона

И наконец, копируем «прозрачную часть» (рис.12)

Рис.15. Итоговый прозрачный контур

Итог. Плачевный

В итоге мы получили такую же ступенчатую внутреннюю часть, как если бы рисовали через GDI Path. Это грустно признавать, но маски, которые необходимы для работы алгоритма, ступенчаты, и они не могут быть в этом случае никакими другими.

P.S. Соблазны

Возникает соблазн вместо шагов 5 и 6 сразу нарисовать чёрный текст на будущей прозрачной части итогового изображения. Это путь губителен, как и любой другой соблазн, потому что приводит к следующим неприятностям:

Рис.16. Соблазн схалтурить приводит к тотальным неприятностям

Оптимизация цикла

У нас сейчас используется цикл полного перебора. Проще говоря N * N или N2.

Если ширина контура = 1, то N=3 (-1,0,1) и алгоритм нарисует в итоге 3 * 3 = 9 надписей. Если ширина контура = 4, то N = 9 (-4,-3,-2,-1,0,1,2,3,4) и в итоге будет 9 * 9 = 81. Замедление при увеличении ширины контура будет расти в геометрической прогрессии.

Давайте произведём модернизацию цикла, уберём уже нарисованные части:

Таким образом у нас получается 2N + 2N — 4 = 4(N-1). Если ширина контура = 4 и N = 9, получаем 4*8 = 32. Ощутимо, по сравнению с 81, правда?

Да я больше скажу, если контур = 1, N = 3, то в итоге получим 8, а не 9.

Сравним результаты. Чтобы не мешалась прозрачность, отключим.

Рис.17. Полный перебор. Время почти 7 мсек
Рис.18. Оптимизация. Время 2.5 мсек

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

Прозрачный контур текста с антиалиасом

Как отрисовку сделать быстрее разобрались. Теперь, как сделать качественней.

Вначале точно также сохраним фон, но в 32-битном варианте:

Затем нарисуем текст в черно-белой гамме, но в 32 битах.

Ровно то же самое, но в 32 битах. Но какая разница с рисунком 11!

Рис.19. 32-битная маска

Когда рисуется шрифт, система применяет некое подобие антиалиаса, чтобы начертание было спокойным, уравновешенным и радовало глаз.

В этой маске, на границе соприкосновения белого с чёрным, существует градиент:

Рис.20. Градиент на границах

Переведём цвета в примитивную серую гамму, путём среднего арифметического, и будем воспринимать результат, как альфа канал.

Рис.21. Карта альфа-канала по градиенту

Теперь перенесём данные градиента (msk) в альфа-канал фона (bmp), который скопировали заранее, до отрисовки текста:

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

Чтобы этот код работал, необходимо, чтобы битмапы совпадали по размерам и были в формате 32 бита. Поэтому во фрагментах кода выше есть комментарий: Важно!

А дальше просто отрисуем поверх:

И в итоге мы получаем очень приятный сглаженный контур (кликнув по картинке, можно рассмотреть поближе):

Рис.22. Финал

Листинг

Вот теперь можно смело копипастить и использовать. Возможно, надо как-то переобозвать во что-то типа DrawTextContour, DrawTextWithBorder и т.д.:


Скачать

Друзья, спасибо за внимание!

Исходник (zip) 81.7 Кб. Delphi XE 7, XE 10, XE 11

Исполняемый файл (zip) 1.0 Мб.


Об использовании и скорости поговорим в другой раз, и так много получилось ))) Обязательно сообщу в телеге, подписывайтесь!!!

5 7 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

2 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Sturman

Ваш перфекционизм впечатляет :). Но лишний раз убедился, что не зря все эти штуки с текстом делаю в GDI+

2
0
Не нашли ответ на свой вопрос? Задайте его здесь!...x