The App Compat Guy

Chris Jackson's Semantic Consonance

May, 2006

  • The App Compat Guy

    Creating Text Labels With a Drop Shadow Effect in Windows Forms

    • 3 Comments

    One great new feature in WinFX / Windows Presentation Foundation (Avalon) is the ability to render true drop shadows using the DropShadowBitmapEffect class. What do I mean by true shadows? It's relatively straightforward to simply render text twice onto a form, but in the real world the edges of shadows do not have hard edges. In the real world, light does not come from a single one dimensional point in three dimensional space. The consequence is that shadows have both an umbra and a penumbra - a darker center, surrounded by a gradient from dark to light at the edges. You get this out of the box with Windows Presentation Foundation. However, even though Windows Presentation Foundation now has a Go Live license, not everyone can take a dependency on a beta framework. What I wanted to do was create such an effect using Windows Forms.

    Given that we can understand the basic premise (darker middle, surrounded by a gradient from dark to light), we could conceivably render this manually. We could conceivably work with GraphicsPath objects to vectorize fonts, create two paths (one representing the outer edge of the penumbra and the other representing the inner edge of the penumbra / outer edge of the umbra, use a PathGradientBrush for the penumbra, and use a SolidBrush for the umbra. But that sure sounds like a lot of work, both for me and for the graphics subsystem of my trusty Tablet PC. (Using a PathGradientBrush is a fairly expensive operation.)

    So my next thought, of course, is ... how can I cheat?

    If you were following my previous article on rasterizing images, we looked at several algorithmic options for scaling raster images. If you played around with any of these algorithms, you will begin to notice some of the blurring effects when scaling up images, and in particular if you use the HighQualityBicubic interpolation mode you end up with a nice fuzzy blur.

    A nice fuzzy blur.

    Around a solid colored center.

    Hmm...

    That sounds a little bit like what we actually want. Perhaps what is normally a bad thing when scaling up images can be put to productive use in this scenario.

    One option for creating more realistic shadows using Windows Forms is to scale down the text that you want when you rasterize it, and then scale it back up using the HighQualityBicubic interpolation mode. Let's take a look at how that looks.

    More Realistic Text Shadows in Windows Forms

    To achieve this, I derived from the Label class. I added a few properties to describe the shadow, and then I overrode the OnPaint method. When I paint, I create a bitmap that is smaller (scaled based on the Softness property), and then apply a matrix transformation to the graphics object for this bitmap. The matrix transformation includes both scaling (so the font appears at the same relative size when scaled back up) and offset based on the Direction and ShadowDepth properties. After I have rendered onto the bitmap, I render the bitmap onto the label itself, and finally render the text directly onto the Label. The result? A more realistic shadow.

    Is this a perfect approach? Far from it. If you manipulate the properties for a while, you will notice that there are sometimes challenges in getting the text to appear at exactly the same size. We expect this. Why? We are rasterizing the same vector two different times. The rasterizer has to determine which pixels are the best fit for the vector at two completely different scales, and you will end up with different rounding errors at each.

    Another shortcoming is that Windows Forms will not allow the Label object to render pixels outside of its bounds. As you ratchet up the ShadowDepth property, the bitmap you render will drift further and further off the edge of the Label object's bounds, initially being cut off, and eventually disappearing entirely.

    Nonetheless, this is an interesting cheat for obtaining a more realistic effect, and something that is kind of fun to play around with.

    Here is the code for my MoreRealisticShadowLabel class:

    namespace SoftShadowLabel {
    
      using System;
      using System.ComponentModel;
      using System.Drawing;
      using System.Drawing.Drawing2D;
      using System.Drawing.Imaging;
      using System.Drawing.Text;
      using System.Windows.Forms;
    
    
      class MoreRealisticShadowLabel : Label {
    
        private Color color;
        private int direction;
        private float softness;
        private int opacity;
        private int shadowDepth;
    
        public MoreRealisticShadowLabel()
          : base() {
          color = Color.Black;
          direction = 315;
          softness = 2f;
          opacity = 100;
          shadowDepth = 4;
        }
    
        [Category("Appearance")]
        [Description("Gets or sets the color of the shadow")]
        [DefaultValue(typeof(Color), "0x000000")]
        public Color Color {
          get {
            return color;
          }
          set {
            color = value;
            Invalidate();
          }
        }
    
        [Category("Appearance")]
        [Description("Gets or sets the degree of opacity of the shadow")]
        [DefaultValue(100)]
        public int Opacity {
          get {
            return opacity;
          }
          set {
            if (value < 0 || value > 255) {
              throw new ArgumentOutOfRangeException("Opacity",
                "Opacity must be between 0 and 255");
            }
            opacity = value;
            Invalidate();
          }
        }
    
        [Category("Appearance")]
        [Description("Gets or sets how soft the shadow is")]
        [DefaultValue(2f)]
        public float Softness {
          get {
            return softness;
          }
          set {
            if (softness <= 0) {
              throw new ArgumentOutOfRangeException("Softness",
                "Softness must be greater than 0");
            }
            softness = value;
            Invalidate();
          }
        }
    
        [Category("Appearance")]
        [Description("Gets or sets the angle the shadow is cast")]
        [DefaultValue(315)]
        public int Direction {
          get {
            return direction;
          }
          set {
            if (value < 0 || value > 360) {
              throw new ArgumentOutOfRangeException("Direction", 
                "Direction must be between 0 and 360");
            }
            direction = value;
            Invalidate();
          }
        }
    
        [Category("Appearance")]
        [Description("Gets or sets the distance between the plane " +
          "of the object casting the shadow and the shadow plane")]
        [DefaultValue(4)]
        public int ShadowDepth {
          get {
            return shadowDepth;
          }
          set {
            if (value < 0) {
              throw new ArgumentOutOfRangeException("ShadowDepth",
                "ShadowDepth must be greater than 0");
            }
            shadowDepth = value;
            Invalidate();
          }
        }
    
        protected override void OnPaint(PaintEventArgs e) {
    
          Graphics screenGraphics = e.Graphics;
          Bitmap shadowBitmap = new Bitmap(Math.Max((int)(Width / softness), 1),
            Math.Max((int)(Height / softness), 1));
          using (Graphics imageGraphics = Graphics.FromImage(shadowBitmap)) {
            imageGraphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
            Matrix transformMatrix = new Matrix();
            transformMatrix.Scale(1 / softness, 1 / softness);
            transformMatrix.Translate((float)(shadowDepth * Math.Cos(direction)),
              (float)(shadowDepth * Math.Sin(direction)));
            imageGraphics.Transform = transformMatrix;
            imageGraphics.DrawString(Text, Font,
              new SolidBrush(Color.FromArgb(opacity, color)), 0, 0,
              StringFormat.GenericTypographic);
          }
          screenGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
          screenGraphics.DrawImage(shadowBitmap, ClientRectangle, 0, 0, 
            shadowBitmap.Width, shadowBitmap.Height, GraphicsUnit.Pixel);
          screenGraphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
          screenGraphics.DrawString(Text, Font, new SolidBrush(ForeColor), 0, 0, 
            StringFormat.GenericTypographic);
      
        }
    
      }
    }
    

    You may have noticed that I am using ClearType for the text rendering, but AntiAliasGridFit for the shadow rendering. If I turn on ClearType when rendering the shadowed text to the bitmap, then things really start to go haywire, and I lose transparency altogether.

    The same principals can be applied to any vector drawing - this technique is not specific to text. You could also apply this technique to a raster image that includes transparency, although you would have to touch each of the pixels to change the color to your shadow color. With both, you also have to determine if you want to have your shadow alpha vary with your object alpha. Here, we assumed a constant alpha value for our text vectors, although this may not necessarily be a safe assumption.

    Let's see if I can talk about something other than rendering text next time around...

  • The App Compat Guy

    GDI vs. GDI+ Text Rendering Performance

    • 9 Comments

    In my last post, I included the unverified claim, "I have heard (but I have not personally verified) that GDI font rendering is approximately 10x faster than GDI+ font rendering (1M glyphs / second vs. 100K glyphs / second). Of course, there is a reason why GDI+ takes more work - text is truly device independent using GDI+. Yet another tradeoff. If you are interested in the details, check out GDI+ Text, Resolution Independence, and Rendering Methods. Or - Why does my text look different in GDI+ and in GDI?." Well, as you can probably guess by the title of this entry, making that unverified claim kept gnawing away at me, so when I finally had some time to cut some code, I decided to investigate that myself. What really is the performance overhead of these features?

    I didn't really have the time to put together a scientific study, but I was OK with that. What I really wanted was a gut check, and if I was correct within an order of magnitude, then this was good enough for me. Rendering performance is, of course, going to change based on font, character, how the characters are put together, and how we leverage any internal optimizations of the framework. To keep things simple, I decided to stick with my favorite font, Segoe UI, using ClearType. Rather than trying to select a single character, or limiting myself to letters of the alphabet, I iterated through ASCII codes from 33 to 126, all of which are printable characters. Rather than constructing arbitrary strings, I just rendered one character at a time. I wanted to render a lot of characters, but I didn't want any potential confounds from overlapping characters, so I spaced them out evenly across the entire screen and maximized my window. I wanted to take a reasonable number of samples, so I handled a mouse click to fire up a timer and invalidated the window 100 times for each rendering type. Finally, I wanted to create this in unmanaged code, for a couple of reasons. First, it eliminates another potential confound. Second, every time I create software in unmanaged code, it reminds me of just how much I prefer using managed code!

    Simple Application for Measuring GDI and GDI+ Font Rendering Performance

    What did I discover? On my Toshiba M200 Tablet PC, using a GeForce FX 5200Go with 32 MB of graphics memory, the absolute values of my numbers were lower. However, the relative values did align with the claims that I published earlier: GDI+ font rendering was approximately an order of magnitude slower than GDI rendering, proving once again that executing code is more expensive than not executing code. Specifically, my GDI code path was rendering approximately 99,000 glyphs per second, while my GDI+ code path was rendering approximately 16,000 glyphs per second.

    Of course, I have rendered my share of text using GDI+ and it was always fast enough that I never thought twice about sacrificing the important features and looking for something faster, but it's always good to know what the options and tradeoffs are. This is something to think long and hard about. There is a big difference between optimizing away waste and optimizing away features. If I have a computer that is much more powerful than what was available when GDI was designed, I could theoretically just run the same applications much, much faster. Alternately, I could run updated versions of those new applications that provide me with tangible (whether or not they are particularly sexy) benefits. Resolution independence is a good thing. Enhanced readability is a good thing. Only when I find an audience who is reading more than 16,000 glyphs per second (some serious speed readers) am I going to start wanting to thinking about the trade-offs. With the implementation I have here, I get noticeably more flicker with the GDI+ implementation than I do with the GDI implementation, but double buffering is a solution I would consider before losing the ability to satisfy a user 2 years from now using a 500 dpi screen who thinks my font rendering is less readable than competing applications.

    For those who are interested, this is the code I used for my little experiment. It's quick and dirty, but it was good enough for my limited purposes.

    #include <WINDOWS.H>
    #include <STRSAFE.H>
    #include <GDIPLUS.H>
    
    #define ID_TIMER 1
    
    using namespace Gdiplus;
    
    LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM);
    
    int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, 
                        LPSTR lpCmdLine, int nShowCmd) {
    
      HWND hwnd;
      MSG msg;
      WNDCLASS wndclass;
      GdiplusStartupInput gdiplusStartupInput;
      ULONG_PTR gdiplusToken;
      static TCHAR szAppName[] = TEXT("GDI vs. GDI+ Performance");
    
      GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
    
      wndclass.style = CS_HREDRAW | CS_VREDRAW;
      wndclass.lpfnWndProc = WndProc;
      wndclass.cbClsExtra = 0;
      wndclass.cbWndExtra = 0;
      wndclass.hInstance = hInstance;
      wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
      wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
      wndclass.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
      wndclass.lpszMenuName = NULL;
      wndclass.lpszClassName = szAppName;
    
      RegisterClass(&wndclass);
    
      hwnd = CreateWindow(szAppName, szAppName, 
        WS_OVERLAPPEDWINDOW | WS_MAXIMIZE, CW_USEDEFAULT, 
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL,
        NULL, hInstance, NULL);
      ShowWindow(hwnd, nShowCmd);
      UpdateWindow(hwnd);
    
      while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
    
      GdiplusShutdown(gdiplusToken);
    
      return msg.wParam;
    }
    
    LRESULT CALLBACK WndProc(HWND hwnd, UINT message, 
                             WPARAM wParam, LPARAM lParam) {
    
      static LOGFONT lf;
      static double gdiDuration = 0;
      static double gdiplusDuration = 0;
      static __int64 gdiGlyphCount = 0;
      static __int64 gdiplusGlyphCount = 0;
      static int timerCounter;
      static bool useGdi = true;
    
      switch (message) {
    
        case WM_CREATE:
          lf.lfWidth = 0;
          lf.lfItalic = FALSE;
          lf.lfQuality = 6; //CLEARTYPE_NATURAL_QUALITY
          StringCchCopy(lf.lfFaceName, 9, TEXT("Segoe UI"));
          return 0;
    
        case WM_LBUTTONDOWN:
          timerCounter = 0;
          gdiDuration = 0;
          gdiplusDuration = 0;
          gdiGlyphCount = 0;
          gdiplusGlyphCount = 0;
          SetTimer(hwnd, ID_TIMER, 100, NULL);
          return 0;
    
        case WM_TIMER:
          if (timerCounter++ < 100) {
            useGdi = true;
            InvalidateRect(hwnd, NULL, TRUE);
          } else if (timerCounter < 200) {
            useGdi = false;
            InvalidateRect(hwnd, NULL, TRUE);
          } else {
            KillTimer(hwnd, ID_TIMER);
            __int64 frequency;
            QueryPerformanceFrequency((LARGE_INTEGER*)&frequency);
            gdiDuration = (double)gdiGlyphCount /
              (gdiDuration / (double)frequency);
            gdiplusDuration = (double)gdiplusGlyphCount /
              (gdiplusDuration / (double)frequency);
            wchar_t durationString[100];
            swprintf_s(durationString, 100, 
              TEXT("GDI:\t%0.5f glyphs/sec\nGDI+:\t%0.5f glyphs/sec"),
              gdiDuration, gdiplusDuration);
            MessageBox(hwnd, durationString, TEXT("Duration"), 0);
          }
          return 0;
    
        case WM_PAINT:
    
          {
    
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);
            RECT clientRect;
            GetClientRect(hwnd, &clientRect);
    
            lf.lfHeight = -MulDiv(12, GetDeviceCaps(hdc, LOGPIXELSY), 72);
            SelectObject(hdc, CreateFontIndirect(&lf));
            TEXTMETRIC tm;
            GetTextMetrics(hdc, &tm);
            RECT charRect;
            charRect.top = 0;
            charRect.bottom = tm.tmHeight;
            char displayChar = 33;
    
            Graphics g(hdc);
            g.SetTextRenderingHint(TextRenderingHintClearTypeGridFit);
            SolidBrush brush(Color(255, 0, 0, 0));
            FontFamily fontFamily(L"Segoe UI");
            Font font(&fontFamily, 12, FontStyleRegular, UnitPoint);
    
            __int64 start;
            __int64 end;
            QueryPerformanceCounter((LARGE_INTEGER*)&start);
    
            while (charRect.bottom < clientRect.bottom) {
              charRect.left = 0;
              charRect.right = tm.tmMaxCharWidth;
              while (charRect.right < clientRect.right) {
                PointF pointF(charRect.left, charRect.top);
                WCHAR charString[2];
                charString[0] = (WCHAR)displayChar;
                charString[1] = '\0';
                if (useGdi) {
                  DrawText(hdc, charString, -1, &charRect, DT_SINGLELINE);
                  gdiGlyphCount++;
                } else {
                  g.DrawString(charString, -1, &font, pointF, &brush);
                  gdiplusGlyphCount++;
                }
                displayChar = displayChar < 126 ? displayChar + 1 : 33;
                charRect.left += tm.tmMaxCharWidth;
                charRect.right += tm.tmMaxCharWidth;
              }
              charRect.top += tm.tmHeight;
              charRect.bottom += tm.tmHeight;
            }
            QueryPerformanceCounter((LARGE_INTEGER*)&end);
            if (useGdi) {
              gdiDuration += (double)end - (double)start;
            } else {
              gdiplusDuration += (double)end - (double)start;
            }
    
            DeleteObject(SelectObject(hdc, GetStockObject(SYSTEM_FONT)));
            EndPaint(hwnd, &ps);
            return 0;
    
          }
    
        case WM_DESTROY:
          PostQuitMessage(0);
          return 0;
      }
      return DefWindowProc(hwnd, message, wParam, lParam);
    }
    

    You may notice one thing about how I am timing: while I have moved a lot of the code outside of the timer, there is still quite a bit inside. This is another tradeoff that I had to make in measurement. If I just timed the rendering calls themselves, then there is a larger percentage of error incorporated into my measurements. Moved outside, as it is now, I gain precision, while at the same time losing it by incorporating non-rendering code in my measurement. Neither option seemed perfect, but the latter seemed the superior approach to me. I did take the time to measure it both ways, however - the outcome with timing each individual rendering call was slightly lower performance numbers: approximately 89,000 glyphs/second with GDI and approximately 10,000 glyphs/second with GDI+. However, the relationship between the two was very similar, so my confidence was not shaken.

    Of course, if you are interested at all in this line of thought, you may also be interested in rendering performance using Windows Presentation Foundation ("Avalon"). However, that is someplace I am not willing to go at this point. First of all, it would be completely unfair to start benchmarking beta code. I certainy wouldn't want to benchmark beta code on a beta operating system running on beta WDDM drivers! Furthermore, as DirectX 10 hardware acceleration begins to arrive in consumer products, this will change dramatically. I remember back during the Windows XP (Whistler) beta the remarkable difference we experienced in some of the new 2D effects (such as the "fade to grey" effect when you are logging out) once video cards and drivers began to support 2D hardware acceleration...

  • The App Compat Guy

    Windows Forms Font Hinting with TextRenderingHint

    • 7 Comments

    After poking around the concepts of raster and vector images, I really wanted to dive a bit deeper into the most common form of vector image you will find today: fonts. Most of the fonts you run into today on a Windows system will be either True Type or Open type fonts, which are vector (outline) fonts. And, just like vector images you draw yourself (or metafiles that contain instructions to draw vector images), fonts have to be rasterized before you can display them on a screen. (Unless, of course, you procured your monitor from an old Asteroids video game machine, in which case it can render true vectors.) Rasterizing fonts has the same considerations as rasterizing any other vector images - when you map an infinitely precise mathematical line segment against a discrete pixel, you need software to determine what each pixel should display. With most vector images, it uses a generic algorithm. The results you obtain using this same generic algorithm against fonts can be disappointing, to say the least.

    When you render text using Windows forms, you can specify the Text Rendering Hint on the Graphics object. I created a short program to iterate through the various options. This is the code that I used:

    protected override void OnPaint(PaintEventArgs e) {
      Graphics graphics = e.Graphics;
      string text = "the quick brown fox jumped over the lazy dog";
      SizeF size = graphics.MeasureString(text, Font);
      using (SolidBrush brush = new SolidBrush(ForeColor)) {
        for (int i = 0; i < 6; i++) {
          graphics.TextRenderingHint = TextRenderingHint.SystemDefault;
          graphics.DrawString(((TextRenderingHint)i).ToString(), Font, brush, 
            new PointF(0, size.Height * i));
          graphics.TextRenderingHint = (TextRenderingHint)i;
          graphics.DrawString(text, Font, brush, new PointF(125, size.Height * i));
        }
      }
    }
    

    Let's take a look at what we come up with. For this discussion, I am not interested in the differences between various anti-aliasing options, such as clear type. Personally, I'm fanatical about turning on ClearType everwhere that I can, but that is beside the point. The differences I am interested in looking at are between the GridFit enumeration values and the rest. (You can think of the GridFit suffix as basically means to use font hinting. However, this isn't entirely true. For grid fitting, the font glyph outlines are actually being moved around to best fit the bitmap grid On top of that, font hinting will be used to make additional suggestions to the rasterizer as to how the glyph should be rendered.)

    8-Point Segoe UI with Various Text Rendering Hints

    As an aside, I am a big fan of the Segoe UI font, which is the system font for Windows Vista and is also the font used by the 2007 Office System. Curiously, while a variant on this font is used for Windows Mobile 5.0 Smartphones, it is not used for Windows Mobile 5.0 Pocket PCs. And, unfortunately, the Palm Treo 700w I use is built on the Pocket PC version. I have absolutely no idea why I am stuck with Tahoma on my phone, which I find much less pleasurable to use. Bummer.

    The differences are much more obvious on smaller fonts, which is why I selected an 8-point font. If you zero in on the SingleBitPerPixel rendering, you will notice that some of the characters aren't even complete! This is where font hinting helps out. It is a way that the developer of the font can make explicit suggestions to improve rendering.

    Why might you care about this? Well, I found a few useful nuggets out of this little exploration.

    First of all, if you are concerned about performance, it's good to know what your costs and trade-offs are. Rendering fonts includes rasterization if you are using an outline font. If you turn on hinting, then you have to actually run code (and yes, hinting code can contain loops) above and beyond rasterization code. But, from a trade-off perspective, it's very clear to me that these are CPU cycles that are very worthwhile! If you use bitmapped fonts (and there still are some in a standard installation of Windows) then you do not have to pay either cost. However, you also can't take advantage of anti-aliasing - either standard or clear type. (Personally, I can't stand to use software that uses bitmapped fonts any more. If it doesn't support clear type, and I am not compelled to use it for some reason, I'll remove the program.)

    Notice that there is an option for ClearType. Normally, you may think of ClearType as a system-wide setting, but its presence here should suggest that you can render fonts using Clear Type even if this smoothing technique has not been enabled system-wide! While normally you would generally prefer to honor the user's system settings, with a reading technology as powerful as Clear Type that wasn't enabled by default when Windows XP was released (and where the setting is potentially non-obvious to somebody who isn't a power user) it is nice to know that you could provide this benefit to your user base without requiring them to discover this setting.

    You probably noticed that the text we are outputting are not all exactly the same length. The GridFit fonts tend to be longer. This isn't entirely unexpected - if we are moving glyphs around to find the best fit for the bitmap grid we are rasterizing to, you probably expect that the movement will more often be to the right (on right to left text) so that you don't end up with overlap and crowding with the previous glyph. However, if you inject some code to use the Graphics.MeasureString API, you will discover that the length of the string is reported as always being the same, even though your eyes tell you differently! This is actually a more complex operation than it seems. To measure the size of a string, are doing a translation from a vector image to a raster image. This translation also includes grid fitting and hinting. Apparently, the MeasureString API is not running all of this code before returning a response, and the most obvious reason why is that it does this in the interest of performance. Considering trade-offs, the question then becomes this - how important is it to be to-the-pixel accurate, compared to the value of the CPU cycles that could otherwise be doing something the user could actually see. For example, I am using the MeasureString API to find an approximation of the string's height in order to space out my output. If the result is 1 pixel greater than it would otherwise need to be, do I really care? In this example, I do not. However, it's good to keep this in mind when you use this API.

    You will also notice that I used the Graphics.DrawString API rather than the TextRender.MeasureText API. TextRenderer internally uses some font caching, as well as calling into the GDI function DrawTextEx. The result is that everything I render is rendered using hinted fonts, which made it a poor choice for the purpose of this demo. Wait ... it's using GDI instead of GDI+? I have heard (but I have not personally verified) that GDI font rendering is approximately 10x faster than GDI+ font rendering (1M glyphs / second vs. 100K glyphs / second). Of course, there is a reason why GDI+ takes more work - text is truly device independent using GDI+. Yet another tradeoff. If you are interested in the details, check out GDI+ Text, Resolution Independence, and Rendering Methods. Or - Why does my text look different in GDI+ and in GDI?.

    Knowing about font hinting is also important if you are using text with GraphicsPath objects. The GraphicsPath object exposes AddString methods, and you can use this to include text in your path. However, internally, a GraphicsPath object contains nothing but a collection of points. There are no exceptions made to store text. That means that the text is stored as the points required to construct this image. Once the text is converted to points, there is no way to differentiate these points from any other points in the path. And this, of course, means that when you rasterize this path, you will be rasterizing the points comprising the text, leveraging none of the grid fitting and hinting provided by fonts. If your fonts are smaller, the outcome could likely be as unattractive and unreadable as the SingleBitPerPixel font rendering.

    The final thing I pondered was this: if the technique of hinting so visibly helpful in rendering font vectors, wouldn't it be just as helpful in rendering arbitrary vectors? I certainly think so. Obviously, the completeness of a font has a much higher impact than the completeness of an arbitrary picture - you want to be able to process a large number of glyphs as efficiently as possible. However, as we slowly (glacially) make our way towards true device independence and vector images, I can foresee a need to support vector hinting on arbitrary vector images, and not fonts alone. I have no idea if or when this is coming...

    Note that I am not an expert in the inner workings of fonts, and that I have discovered many of these things during the course of my exploration. I just need to use them. If you have something to add or correct, please feel free to comment, trackback, or email!

Page 1 of 1 (3 items)