📜 ⬆️ ⬇️

Typography and WPF - Draw beautiful text

Important: this approach is outdated, now you can just use DirectWrite and get all the OpenType buns. An example of a specific implementation can be found here .



Introduction


As you know, WPF has a fairly powerful built-in typography system. Unfortunately, this system is mainly focused on working with documents and, thus, all typographic delights like OpenType support cannot be used for some simple controls like Label. But, no matter what, there is still an opportunity to get high-quality text rendering - you just need to suffer a little.

Task


Why bother to draw text at all? Well, for example, I want to have beautiful headlines on the blog - made by the fonts that I chose. Of course, there are already solutions based on images or Flash, but they are either resource-intensive (like drawing SVG, for example), or incompatible with IE (for example, those using the Canvas element). In addition, none of the available systems support either OpenType or ClearType, that is, it is not convenient for small text, and does not allow you to fully depreciate your investments in expensive fonts 1 .

Decision


In order to get our headlines, we will use the WPF typographic system. Since our goals are rather primitive, we will use only three main classes:



The three constructs described above are very easy to use. Here is a fairly simple example:


')
// <br/>
Run r = new Run( "Hello rich WPF typography" );<br/>
// <br/>
Paragraph p = new Paragraph();<br/>
p.Inlines.Add( r );<br/>
// <br/>
FlowDocument fd = new FlowDocument();<br/>
fd.Blocks.Add(p);<br/>

Subpixel optimization


At this stage, we could just draw our FlowDocument into the texture, but then we would get a simple black and white antialiasing, while with sub-pixel rendering, the text looks much clearer.



Let's look at how you can get an effect similar to ClearType. First, since we need to get 3 times more information horizontally, let's stretch our text so that it is 3 times wider.



DocumentPaginator dp = ((IDocumentPaginatorSource)fd).DocumentPaginator;<br/>
ContainerVisual cv = new ContainerVisual();<br/>
cv.Transform = new ScaleTransform(3.0, 1.0);<br/>
cv.Children.Add(dp.GetPage(0).Visual);<br/>

So, we created a container for Visual elements (for the visual component of the document), pulled the first page out of the document, placed it in this ContainerVisual , and stretched it 3 times horizontally. Everything is good, but for now it’s just a Visual that needs to be drawn somehow. Not a problem - for this there is a corresponding API that draws Visual directly into a bitmap. Or rather not in Bitmap but in RenderTargetBitmap :



// . 3<br/>
RenderTargetBitmap rtb = new RenderTargetBitmap(2400, 100, 72, 72, PixelFormats.Pbgra32);<br/>
rtb.Render(cv);<br/>

Perhaps this is where the “whims” of WPF begin, because there is no direct conversion into the System.Drawing.Bitmap we are used to. But this is nothing - it is enough to serialize the data into a stream and then get them from this stream and we will get essentially the same thing:



PngBitmapEncoder enc = new PngBitmapEncoder();<br/>
enc.Frames.Add(BitmapFrame.Create(rtb));<br/>
Bitmap zeroth;<br/>
using (MemoryStream ms = new MemoryStream())<br/>
{<br/>
// <br/>
enc.Save(ms);<br/>
// <br/>
zeroth = new Bitmap(ms);<br/>
}<br/>

So, we got a “zero” bitmap, that is, a stove, from which we will dance. If you now take and save the bitmap, you get something like this:





One should not be surprised - it is really just a text that is stretched 3 times using the typographical system WPF. Now, in order to prepare our picture for sub-pixel optimization, let's distribute the energy of each pixel to its neighbors - two on the left and two on the right 2 . This will allow us to make a very smooth, not annoying user, drawing. To do this, create a useful structure called argb :

public struct argb<br/>
{<br/>
public int a, r, g, b;<br/>
public void AddShift(Color color, int shift)<br/>
{<br/>
a += color.A >> shift;<br/>
r += color.R >> shift;<br/>
g += color.G >> shift;<br/>
b += color.B >> shift;<br/>
}<br/>
}<br/>

This structure has only one purpose - to take the constituent elements of a certain Color , to modulate its parameters by shifting, and to record the result. And now we will use this structure:



public Bitmap Coalesce(Bitmap bmp)<br/>
{<br/>
int width = bmp.Width;<br/>
int height = bmp.Height;<br/>
Bitmap output = new Bitmap(width, height);<br/>
for ( int y = 0; y < height; ++y)<br/>
{<br/>
for ( int x = 2; x < width - 2; ++x)<br/>
{<br/>
argb final = new argb();<br/>
final.AddShift(bmp.GetPixel(x - 2, y), 3);<br/>
final.AddShift(bmp.GetPixel(x - 1, y), 2);<br/>
final.AddShift(bmp.GetPixel(x, y), 1);<br/>
final.AddShift(bmp.GetPixel(x + 1, y), 2);<br/>
final.AddShift(bmp.GetPixel(x + 2, y), 3);<br/>
output.SetPixel(x, y, System.Drawing.Color.FromArgb(<br/>
Clamp(final.a),<br/>
Clamp(final.r),<br/>
Clamp(final.g),<br/>
Clamp(final.b)));<br/>
}<br/>
}<br/>
return output;<br/>
}<br/>


Above, we also used the Clamp() function, which ensures that the color value is always less than or equal to 255.



If we now look at this text again, then we will not see anything interesting - just the text is a bit "smeared" horizontally:





The next stage is to get the final image, i.e. squeeze it horizontally 3 times, using the alpha values ​​of the subpixels as red, blue, and green, respectively. The only amendment is that we need to invert these alpha values, that is, subtract the value from 255:



Bitmap second = new Bitmap(( int )(first.Width / 3), first.Height);<br/>
for ( int y = 0; y < first.Height; ++y)<br/>
{<br/>
for ( int x = 0; x < second.Width; ++x)<br/>
{<br/>
// -, 255<br/>
System.Drawing.Color final = System.Drawing.Color.FromArgb(255,<br/>
255 - first.GetPixel(x * 3, y).A,<br/>
255 - first.GetPixel(x * 3 + 1, y).A,<br/>
255 - first.GetPixel(x * 3 + 2, y).A);<br/>
second.SetPixel(x, y, final);<br/>
}<br/>
}<br/>


The last thing you can do is trim the bitmap. That's all:





As a result, we got text with ClearType support. And as for OpenType support, it's simple. For example, on my website I use this script:



Run r1 = new Run(text.Substring(0, 1))<br/>
{<br/>
FontFamily = new FontFamily(fontName),<br/>
FontSize = fontSize,<br/>
FontStyle = FontStyles.Italic<br/>
};<br/>
if ( char .IsLetter(text[0]))<br/>
r1.SetValue(Typography.StandardSwashesProperty, 1);<br/>
Run r2 = new Run(text.Substring(1, text.Length - 2))<br/>
{<br/>
FontFamily = new FontFamily(fontName),<br/>
FontSize = fontSize,<br/>
FontStyle = FontStyles.Italic<br/>
};<br/>
r2.SetValue(Typography.NumeralStyleProperty, FontNumeralStyle.OldStyle);<br/>
Run r3 = new Run(text.Substring(text.Length - 1))<br/>
{<br/>
FontFamily = new FontFamily(fontName),<br/>
FontSize = fontSize,<br/>
FontStyle = FontStyles.Italic<br/>
};<br/>
r3.SetValue(Typography.StylisticAlternatesProperty, 1);<br/>


I think everything is clear here without words - the first letter of the title uses a “handwritten” form, and the last one is an alternative one. You can apply to our headline:





However, since our header has no alternative for the final letter s, we can take something more “indicative”:





Conclusion


Examples of using all of the above are in my blog - I use this subsystem to generate titles. And before you ask - no, such an approach does not interfere with indexing (if you do not believe, make a 'view source' and see how it is implemented), and there are no problems with “readers” like Google Reader either. On the other hand, on my blog you can see some system bugs that I’m currently fixing.



What I described above is a terribly slow approach. The functions GetPixel() and SetPixel() are a real evil, and in a good way all manipulations with bitmaps should be done in C ++ using OpenMP or Intel TBB. But in my case, the image needs to be generated only once (and I generate it - right after I add the blog entry), so I don’t care. And getting the bytes out of the bitmap and processing them through P / Invoke is easy.



Notes


  1. ↑

    In addition, some systems require you to upload your fonts to the developers on the site.

  2. ↑

    This algorithm is taken from here . The only difference is that I used those coefficients that are easier to use in order to get by with a shift instead of a multiplication.



Petersburg Group ALT.NET

Source: https://habr.com/ru/post/58627/


All Articles