Тема навеяна краткой заметкой Яна Грифитса:
http://www.interact-sw.co.uk/iangblog/2004/07/22/longhorncleartype
В которой он выразил восхищение алгоритмом ClearType в Longhorn. Кроме того, Дэйв Гибсон подробно исследовал, как работает ClearType вообще:
http://www.grc.com/ctwhat.htm
В методе Дейва, так же как и в ClearType XP анти-алиасинг
не используется, он получается за счет втрое большего разрешения по X. Вот Ян Грифитс и был этим фактом недоволен, о чем он так же писал в более ранней заметке.
И вот я думал-подумал и решил, что если взять метод Гибсона и скомбинировать его со сглаженными шрифтами, то как раз и получится Longhorn ClearType. Так оно и есть и я могу с уверенностью утверждать, что используемый мной метод всяко лучше, чем в XP. При этом я не использую ничего, кроме простого C++ и WinAPI.
Замечания.
0. На CRT-мониторах (любых, включая Trinitron) не работает, увы.
1. Дабы не усложнять код, используется R-G-B порядок и соответственно, 24бит TrueColor. Расчитано тоже только на R-G-B порядок цветовых триад в LCD-дисплеях (кто-нибудь видел B-G-R?). Соответственно, приходится приводить буфер к B-G-R, который по непонятным причинам используется в WinPAI. Для работы непосредственно в B-G-R или в 32-битовом пространстве с альфа-каналом, надо модифицировать
blend_lcd_span. Это может быть не так-то просто как кажется.
2. Цвет может быть полупрозрачным, то есть поддерживается полноценый альфа-блендинг
3. Нет никакого отсечения, соответственно при выходе за границы — падает. Отсечение — задача тривиальная и не входит в рамки данной заметки.
4. Работает сравнительно тормозно. Это обусловлено во-первых, жуткой тормознутостью GetGlyphOutline(), во-вторых, усреднением знечений (распределение энергии) по методу Стива Гибсона. Если сделать кэширование бит-мапов после
prepare_lcd_glyph, то скорость станет порядка 250-300 тысяч мелких символов в секунду, то есть, вполне сравнимой с родным HW-accelerated выводом такста.
5. Полный проект находится здесь:
http://antigrain.com/stuff/lcd_font.zip
6. Что получается:
//---------------------------------
// A simple helper class to create font, select object
// and properly destroy it.
class lcd_font
{
public:
~lcd_font()
{
::SelectObject(m_hdc, m_old_font);
::DeleteObject(m_font);
}
lcd_font(HDC hdc, const char* typeface, int height, bool bold=false, bool italic=false) :
m_hdc(hdc),
m_font(::CreateFont(height, // height of font
0, // average character width
0, // angle of escapement
0, // base-line orientation angle
bold ? 700 : 400, // font weight
italic, // italic attribute option
FALSE, // underline attribute option
FALSE, // strikeout attribute option
ANSI_CHARSET, // character set identifier
OUT_DEFAULT_PRECIS, // output precision
CLIP_DEFAULT_PRECIS, // clipping precision
ANTIALIASED_QUALITY, // output quality
FF_DONTCARE, // pitch and family
typeface)), // typeface name
m_old_font(::SelectObject(m_hdc, m_font))
{
}
private:
HDC m_hdc;
HFONT m_font;
HGDIOBJ m_old_font;
};
// Possible formats for GetGlyphOutline() and corresponding
// numbers of levels of gray.
//---------------------------------
struct ggo_gray2 { enum { num_levels = 5, format = GGO_GRAY2_BITMAP }; };
struct ggo_gray4 { enum { num_levels = 17, format = GGO_GRAY4_BITMAP }; };
struct ggo_gray8 { enum { num_levels = 65, format = GGO_GRAY8_BITMAP }; };
// Sub-pixel energy distribution lookup table.
// See description by Steve Gibson: http://grc.com/cttech.htm
// The class automatically normalizes the coefficients
// in such a way that primary + 2*secondary + 3*tertiary = 1.0
// Also, the input values are in range of 0...NumLevels, output ones
// are 0...255
//---------------------------------
template<class GgoFormat> class lcd_distribution_lut
{
public:
lcd_distribution_lut(double prim, double second, double tert)
{
double norm = (255.0 / (GgoFormat::num_levels - 1)) / (prim + second*2 + tert*2);
prim *= norm;
second *= norm;
tert *= norm;
for(unsigned i = 0; i < GgoFormat::num_levels; i++)
{
m_primary[i] = (unsigned char)floor(prim * i);
m_secondary[i] = (unsigned char)floor(second * i);
m_tertiary[i] = (unsigned char)floor(tert * i);
}
}
unsigned primary(unsigned v) const { return m_primary[v]; }
unsigned secondary(unsigned v) const { return m_secondary[v]; }
unsigned tertiary(unsigned v) const { return m_tertiary[v]; }
static unsigned ggo_format()
{
return GgoFormat::format;
}
private:
unsigned char m_primary[GgoFormat::num_levels];
unsigned char m_secondary[GgoFormat::num_levels];
unsigned char m_tertiary[GgoFormat::num_levels];
};
// This function prepares the alpha-channel information
// for the glyph averaging the values in accordance with
// the method suggested by Steve Gibson. The function
// extends the width by 4 extra pixels, 2 at the beginning
// and 2 at the end. Also, it doesn't align the new width
// to 4 bytes, that is, the output gm.gmBlackBoxX is the
// actual width of the array.
//---------------------------------
template<class LutType>
void prepare_lcd_glyph(const LutType& lut,
const unsigned char* gbuf1,
const GLYPHMETRICS& gm,
unsigned char* gbuf2,
GLYPHMETRICS* gm2)
{
unsigned src_stride = (gm.gmBlackBoxX + 3) / 4 * 4;
unsigned dst_width = src_stride + 4;
memset(gbuf2, 0, dst_width * gm.gmBlackBoxY);
for(unsigned y = 0; y < gm.gmBlackBoxY; ++y)
{
const unsigned char* src_ptr = gbuf1 + src_stride * y;
unsigned char* dst_ptr = gbuf2 + dst_width * y;
unsigned x;
for(x = 0; x < gm.gmBlackBoxX; ++x)
{
unsigned v = *src_ptr++;
dst_ptr[0] += lut.tertiary(v);
dst_ptr[1] += lut.secondary(v);
dst_ptr[2] += lut.primary(v);
dst_ptr[3] += lut.secondary(v);
dst_ptr[4] += lut.tertiary(v);
++dst_ptr;
}
}
gm2->gmBlackBoxX = dst_width;
}
// Color struct
//---------------------------------
struct rgba
{
rgba() : r(0), g(0), b(0), a(255) {}
rgba(unsigned char r_, unsigned char g_, unsigned char b_, unsigned char a_=255) :
r(r_), g(g_), b(b_), a(a_) {}
unsigned char r,g,b,a;
};
// Blend one span into the R-G-B 24 bit frame buffer
// For the B-G-R byte order or for 32-bit buffers modify
// this function accordingly. The general idea is 'span'
// contains alpha values for individual color channels in the
// R-G-B order, so, for the B-G-R order you will have to
// choose values from the 'span' array differently
//---------------------------------
void blend_lcd_span(int x,
int y,
const unsigned char* span,
int width,
const rgba& color,
unsigned char* rgb24_buf,
unsigned rgb24_stride)
{
unsigned char* p = rgb24_buf + rgb24_stride * y + x;
unsigned char rgb[3] = { color.r, color.g, color.b };
int i = x % 3;
do
{
int a0 = int(*span++) * color.a;
*p++ = (unsigned char)((((rgb[i++] - *p) * a0) + (*p << 16)) >> 16);
if(i > 2) i = 0;
}
while(--width);
}
// Blend one rectangular glyph
//---------------------------------
void blend_lcd_glyph(const unsigned char* gbuf,
int x,
int y,
const rgba& color,
const GLYPHMETRICS& gm,
unsigned char* rgb24_buf,
unsigned rgb24_stride)
{
for(unsigned i = 0; i < gm.gmBlackBoxY; i++)
{
blend_lcd_span(x + gm.gmptGlyphOrigin.x,
y + gm.gmptGlyphOrigin.y - i,
gbuf + gm.gmBlackBoxX * i,
gm.gmBlackBoxX,
color,
rgb24_buf,
rgb24_stride);
}
}
// Draw a text string in the frame buffer
//---------------------------------
template<class LutType, class CharT>
void draw_lcd_text(HDC hdc,
const LutType& lut,
int x,
int y,
const rgba& color,
const CharT* str,
unsigned char* rgb24,
unsigned stride)
{
// Create an affine matrix with 3x horizontal scaling.
// 3x means that we interpret each pixel in the resulting glyph
// in such a way that it corresponds to the sublixel
// (red, green, or blue), but not to the whole pixel.
//----------------------------------
MAT2 scale3h;
memset(&scale3h, 0, sizeof(MAT2));
scale3h.eM11.value = 3;
scale3h.eM22.value = 1;
// Allocate buffers for glyphs
// In reality use some smarter strategy to detect
// the size of the buffer
unsigned gbuf_size = 16*1024;
unsigned char* gbuf1 = new unsigned char [gbuf_size];
unsigned char* gbuf2 = new unsigned char [gbuf_size];
while(*str)
{
GLYPHMETRICS gm;
int total_size = GetGlyphOutline(hdc,
*str,
lut.ggo_format(),
&gm,
gbuf_size,
(void*)gbuf1,
&scale3h);
if(total_size >= 0)
{
prepare_lcd_glyph(lut, gbuf1, gm, gbuf2, &gm);
blend_lcd_glyph(gbuf2, x, y, color, gm, rgb24, stride);
}
else
{
// GetGlyphOutline() fails when being called for
// GGO_GRAY8_BITMAP and white space (stupid Microsoft).
// It doesn't even initialize the glyph metrics
// structure. So, we have to query the metrics
// separately (basically we need gmCellIncX).
total_size = GetGlyphOutline(hdc,
*str,
GGO_METRICS,
&gm,
gbuf_size,
(void*)gbuf1,
&scale3h);
}
x += gm.gmCellIncX;
++str;
}
delete [] gbuf2;
delete [] gbuf1;
}
// Swap Blue and Red, that is convert RGB->BGR or BGR->RGB
//---------------------------------
void swap_rb(unsigned char* buf, unsigned width, unsigned height, unsigned stride)
{
unsigned x, y;
for(y = 0; y < height; ++y)
{
unsigned char* p = buf + stride * y;
for(x = 0; x < width; ++x)
{
unsigned char v = p[0];
p[0] = p[2];
p[2] = v;
p += 3;
}
}
}
Далее — пример обработчика WM_PAINT:
case WM_PAINT:
{
hdc = BeginPaint(hWnd, &ps);
RECT rt;
GetClientRect(hWnd, &rt);
int width = rt.right - rt.left;
int height = rt.bottom - rt.top;
//Create compatible DC and a bitmap to render the image
//--------------------------------------
BITMAPINFO bmp_info;
bmp_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmp_info.bmiHeader.biWidth = width;
bmp_info.bmiHeader.biHeight = height;
bmp_info.bmiHeader.biPlanes = 1;
bmp_info.bmiHeader.biBitCount = 24;
bmp_info.bmiHeader.biCompression = BI_RGB;
bmp_info.bmiHeader.biSizeImage = 0;
bmp_info.bmiHeader.biXPelsPerMeter = 0;
bmp_info.bmiHeader.biYPelsPerMeter = 0;
bmp_info.bmiHeader.biClrUsed = 0;
bmp_info.bmiHeader.biClrImportant = 0;
HDC mem_dc = ::CreateCompatibleDC(hdc);
void* buf = 0;
HBITMAP bmp = ::CreateDIBSection(
mem_dc,
&bmp_info,
DIB_RGB_COLORS,
&buf,
0,
0
);
HBITMAP temp = (HBITMAP)::SelectObject(mem_dc, bmp);
// Calculate image stride and size
//---------------------------------
unsigned char* rgb24 = (unsigned char*)buf;
unsigned rgb24_stride = (width * 3 + 3) / 4 * 4;
unsigned rgb24_size = height * rgb24_stride;
// Clear the image
//---------------------------------
memset(rgb24, 255, rgb24_size);
// Create the energy distribution lookup table.
// See description by Steve Gibson: http://grc.com/cttech.htm
// The class automatically normalizes the coefficients
// in such a way that primary + 2*secondary + 3*tertiary = 1.0
// Also, the input values are in range of 0...64, output ones
// are 0...255.
//
// Try to play with different coefficients for the primary,
// secondary, and tertiary distribution weights.
// Steve Gibson recommends 1/3, 2/9, and 1/9, but it produces
// too blur edges. It's better to increase the weight of the
// primary and secondary pixel, then the text looks much crisper
// with inconsiderably increased "color fringing".
//---------------------------------
//lcd_distribution_lut<ggo_gray8> lut(1.0/3.0, 2.0/9.0, 1.0/9.0);
lcd_distribution_lut<ggo_gray8> lut(0.5, 0.25, 0.125);
// Use a separate block to make sure the font will be created,
// used with current DC and destroyed correctly (we need to
// SelectObject(prev_font) before destroying)
//---------------------------------
{
// Draw text
//---------------------------------
lcd_font fnt(hdc, "Arial", -20, false, true);
draw_lcd_text(hdc,
lut,
50 * 3, // X-positioning is also sub-pixel!
100,
rgba(0,30,40, 230),
"Hello World! Welcome to the perfectly LCD-optimized text rendering!",
rgb24, rgb24_stride);
}
{
// Draw "Copyright"
//---------------------------------
lcd_font fnt(hdc, "Arial", -12, false, false);
draw_lcd_text(hdc, lut,
120 * 3, // X-positioning is also sub-pixel!
80,
rgba(50,20,0, 220),
L"\xA9 Maxim Shemanarev http://antigrain.com",
rgb24, rgb24_stride);
}
{
// Draw the big "o"
//---------------------------------
lcd_font fnt(hdc, "Arial", -100, false, false);
draw_lcd_text(hdc, lut,
50 * 3, // X-positioning is also sub-pixel!
10,
rgba(0,0,0),
"O",
rgb24, rgb24_stride);
}
// The drawing method assumes the R-G-B byte order,
// so that we have to change it to the native Windows one
// (B-G-R) to obtain the correct result.
//-------------------------------------------------
swap_rb(rgb24, width, height, rgb24_stride);
// Display the image. If the image is B-G-R-A (32-bits per pixel)
// one can use AlphaBlend instead of BitBlt. In case of AlphaBlend
// one also should clear the image with zero alpha, i.e. rgba8(0,0,0,0)
//-------------------------------------------------
::BitBlt(
hdc,
rt.left,
rt.top,
width,
height,
mem_dc,
0,
0,
SRCCOPY
);
// Free resources
::SelectObject(mem_dc, temp);
::DeleteObject(bmp);
::DeleteObject(mem_dc);
EndPaint(hWnd, &ps);
}
break;
Необходимое примечание:
ClearType is a trademark of Microsoft Corporation, Redmond, WA