Typography and WPF - Draw beautiful text

    Important: this approach is outdated, now you can just use DirectWrite and get all 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, therefore, all typographical delights like OpenType support cannot be used for any simple control like Label. But, no matter what, there is still the opportunity to get high-quality rendering of the text - you just need to torment a little.

    Task


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

    Decision


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


    • Run- This class contains a minimal length of text in WPF and is an analogue in HTML. Everyone Runcan have their own typographic style, which allows us to mix bold text, italics and other styles in one sentence when layout.
    • Paragraph - this is a paragraph or some analogue
      in HTML. Its essence is to contain several Runs (and not only) and show them as one. Since we plan to make headings, one paragraph is enough for us.
    • FlowDocument- This is a document that may contain paragraphs. In fact, this is a kind of container that holds different text blocks and can adapt, for example, to different page sizes. We don’t really need this, but the document as a container is useful to us, because we will pull out the visual information (that is, the texture) from it.

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



    // строка

    Run r = new Run("Hello rich WPF typography");

    // параграф

    Paragraph p = new Paragraph();

    p.Inlines.Add( r );

    // весь документ

    FlowDocument fd = new FlowDocument();

    fd.Blocks.Add(p);

    Subpixel optimization


    At this stage, we could just draw ours FlowDocumentinto the texture, but then we would get a simple black and white antialiasing, while with subpixel rendering the text looks much sharper.



    Let's look at how you can get an effect similar to ClearType. Firstly, 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;

    ContainerVisual cv = new ContainerVisual();

    cv.Transform = new ScaleTransform(3.0, 1.0);

    cv.Children.Add(dp.GetPage(0).Visual);

    So, we created a certain container for the Visualelements (for the visual component of the document), pulled out the first page from the document, placed it in this one ContainerVisual, and stretched it 3 times horizontally. All is well, but so far this is just one Visualthat needs to be drawn somehow. Not a problem - for this there is a corresponding API that draws Visualdirectly into the bitmap. Or rather not in Bitmapbut in RenderTargetBitmap:



    // рисуем. не забудьте умножить конечную ширину на 3

    RenderTargetBitmap rtb = new RenderTargetBitmap(2400, 100, 72, 72, PixelFormats.Pbgra32);

    rtb.Render(cv);

    Perhaps here the "whims" of WPF begin, because we System.Drawing.Bitmapdon’t have a direct conversion to the familiar one . But it’s nothing - just serialize the data into a stream and then get it from this stream and we will essentially get the same thing:



    PngBitmapEncoder enc = new PngBitmapEncoder();

    enc.Frames.Add(BitmapFrame.Create(rtb));

    Bitmap zeroth;

    using (MemoryStream ms = new MemoryStream())

    {

      // пишем все байты в поток

      enc.Save(ms);

      // из этого же потока создаем битмап

      zeroth = new Bitmap(ms);

    }

    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 - this is really just text stretched 3 times using the WPF printing system. 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

    {

      public int a, r, g, b;

      public void AddShift(Color color, int shift)

      {

        a += color.A >> shift;

        r += color.R >> shift;

        g += color.G >> shift;

        b += color.B >> shift;

      }

    }

    This structure has only one purpose - there is no one to take the constituent elements Color, modulate its parameters by shifting, and record the result. Now let's use this structure:



    public Bitmap Coalesce(Bitmap bmp)

    {

      int width = bmp.Width;

      int height = bmp.Height;

      Bitmap output = new Bitmap(width, height);

      for (int y = 0; y < height; ++y)

      {

        for (int x = 2; x < width - 2; ++x)

        {

          argb final = new argb();

          final.AddShift(bmp.GetPixel(x - 2, y), 3);

          final.AddShift(bmp.GetPixel(x - 1, y), 2);

          final.AddShift(bmp.GetPixel(x, y), 1);

          final.AddShift(bmp.GetPixel(x + 1, y), 2);

          final.AddShift(bmp.GetPixel(x + 2, y), 3);

          output.SetPixel(x, y, System.Drawing.Color.FromArgb(

                                  Clamp(final.a),

                                  Clamp(final.r),

                                  Clamp(final.g),

                                  Clamp(final.b)));

        }

      }

      return output;

    }



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



    If we look at this text again now, we won’t see anything interesting - the text just “slightly blurred” horizontally:





    The next step 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 correction is that we need to invert these alpha values, i.e. subtract the value from 255:



    Bitmap second = new Bitmap((int)(first.Width / 3), first.Height);

    for (int y = 0; y < first.Height; ++y)

    {

      for (int x = 0; x < second.Width; ++x)

      {

        // насыщение берем из альфа-значений, а самой альфе присваиваем 255

        System.Drawing.Color final = System.Drawing.Color.FromArgb(255,

          255 - first.GetPixel(x * 3, y).A,

          255 - first.GetPixel(x * 3 + 1, y).A,

          255 - first.GetPixel(x * 3 + 2, y).A);

        second.SetPixel(x, y, final);

      }

    }



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





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



    Run r1 = new Run(text.Substring(0, 1))

    {

      FontFamily = new FontFamily(fontName),

      FontSize = fontSize,

      FontStyle = FontStyles.Italic

    };

    if (char.IsLetter(text[0]))

      r1.SetValue(Typography.StandardSwashesProperty, 1);

    Run r2 = new Run(text.Substring(1, text.Length - 2))

    {

      FontFamily = new FontFamily(fontName),

      FontSize = fontSize,

      FontStyle = FontStyles.Italic

    };

    r2.SetValue(Typography.NumeralStyleProperty, FontNumeralStyle.OldStyle);

    Run r3 = new Run(text.Substring(text.Length - 1))

    {

      FontFamily = new FontFamily(fontName),

      FontSize = fontSize,

      FontStyle = FontStyles.Italic

    };

    r3.SetValue(Typography.StylisticAlternatesProperty, 1);



    I think everything is clear without words - the first letter of the heading uses the "handwritten" form, and the last alternative. Can apply to our header:





    However, since our heading has no alternative for the final letter s, you can take something more "indicative":





    Conclusion


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



    What I described above is an awfully slow approach. The functions GetPixel()and SetPixel()are 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 picture needs to be generated only once (moreover, I generate it - right after I add the blog entry), so I don’t care. And to extract bytes from the bitmap and process them through P / Invoke is not difficult.



    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 to get around the shift instead of multiplying.



    Petersburg Group ALT.NET


    Also popular now: