Using DrawString to top align text

I want to use DrawString to vertically align text with different size fonts. Basically I am trying to print a sell price on a report with the cents printed in a smaller font than than the dollars. This is in a report engine and I need to write to an image, not a Windows Form so I believe I have to use DrawString rather than using TextRenderer.

I have found various articles explaining how to draw text on the BaseLine. These seem to work OK although I am finding that the same font at different sizes can be out by a pixel. I thought I could take these and work out how to align using the ascent design info for the font but I haven't been successful :-(

Does anyone have some sample code that would help?

Update 1: Change the total to reflect that I want top align not vertically aligned. I want the top of the dollar value printed in say Arial 20 to horizontally align with the top of the cents value in Arial 14. All fonts that I have tried have had a problem.

In the following image, I am using Arial Black.

Top aligned text

You can see with the larger fonts there is a gap between the red line and top of 1 but that disappears by the time it gets to the smaller fonts.

Arial With Arial it starts above the line.

Verdana And I think this one was Verdana which starts with a bigger gap at the bigger fonts.

For these three I am using the following code:

float offset = myFont.SizeInPoints /
myFont.FontFamily.GetEmHeight(myFont.Style) *
(myFont.FontFamily.GetLineSpacing(myFont.Style) - myFont.FontFamily.GetCellAscent(myFont.Style));
float pixels = e.Graphics.DpiY / 72f * offset;
float baseline = pixels;// (int)(pixels + 0.5f);

PointF renderPt = new PointF(left, y - baseline);
e.Graphics.DrawString("$1", myFont, new SolidBrush(Color.Black), renderPt);

I also used this sample as a basis for some tests. I figured if the way the ascent line was drawn was accurate then I could simply adjust the initial write point. Alas, when you go to bigger font sizes or different fonts, the ascent line is drawn inaccurately and so I couldn't pursue that path.

I haven't used TextRenderer as I couldn't work out how to get it to work since I am not using a windows form OnPaint event and couldn't work out how to get the appropriate graphics. Been a bit pressed for time but I think that might have top be my next option.

Upvotes: 0

Views: 3530

Answers (2)

user2979790
user2979790

Reputation: 81

//-----------------------------------------------------------------------------------------------------------------
// MeasureLeading Function
// Measures the amount of white space above a line of text, in pixels. This is accomplished by drawing the text
// onto an offscreen bitmap and then looking at each row of pixels until a non-white pixel is found.
// The y coordinate of that pixel is the result. This represents the offset by which a line of text needs to be
// raised vertically in order to make it top-justified.
//-----------------------------------------------------------------------------------------------------------------

public static int MeasureLeading(string Text, Font Font) {

  Size sz = MeasureText(Text, Font);
  Bitmap offscreen = new Bitmap(sz.Width, sz.Height);
  Graphics ofg = Graphics.FromImage(offscreen);
  ofg.FillRectangle(new SolidBrush(Color.White), new Rectangle(0, 0, sz.Width, sz.Height));
  ofg.DrawString(Text, Font, new SolidBrush(Color.Black), 0, 0, StringFormat.GenericTypographic);

  for (int iy=0; iy<sz.Height; iy++) {
    for (int ix=0; ix<sz.Width; ix++) {
      Color c = offscreen.GetPixel(ix, iy);
      if ((c.R!=255) || (c.G!=255) || (c.B!=255)) return iy;
    }
  }

  return 0;
}

//-----------------------------------------------------------------------------------------------------------------
// MeasureText Method
// TextRenderer.MeasureText always adds about 1/2 em width of white space on the right,
// even when NoPadding is specified. But it returns zero for an empty string.
// To get the true string width, we measure the width of a string containing a single period
// and subtract that from the width of our original string plus a period.
//-----------------------------------------------------------------------------------------------------------------

public static System.Drawing.Size MeasureText(string Text, System.Drawing.Font Font) {
  System.Windows.Forms.TextFormatFlags flags
    = System.Windows.Forms.TextFormatFlags.Left
    | System.Windows.Forms.TextFormatFlags.Top
    | System.Windows.Forms.TextFormatFlags.NoPadding
    | System.Windows.Forms.TextFormatFlags.NoPrefix;
  System.Drawing.Size szProposed = new System.Drawing.Size(int.MaxValue, int.MaxValue);
  System.Drawing.Size sz1 = System.Windows.Forms.TextRenderer.MeasureText(".", Font, szProposed, flags);
  System.Drawing.Size sz2 = System.Windows.Forms.TextRenderer.MeasureText(Text + ".", Font, szProposed, flags);
  return new System.Drawing.Size(sz2.Width - sz1.Width, sz2.Height);
}

Upvotes: 1

user1693593
user1693593

Reputation:

Fonts is an area that has inherited much from the physical world with basis in history. unfortunately that is not always compatible with how computers work.

For instance, a font size in the physical world is not necessary equal to the same font size in the physical world (no typo). Take these glyphs at the same size:

G helvetica G script

Although these are the same size (64 points) they are not equal in size. This has to do with the historical aspect of typefaces, where in the physical world glyphs where placed on square metal plates. It was the size of these plates the size referred to, not the glyph on it - they could fill the whole plate; or not. This is also the case with computer based typefaces. You can see the bounding box for the glyphs are the same (=font size), but the glyphs them self are different.

This is usually not a problem with typography or print as one can quickly make adjustment to compensate for this.

With drawing in .net/GDI+ it is a different matter in special cases as this. The baseline is "always correct", that is: you are guaranteed the same alignment if you use the baseline, but of the "bottom" of the glyph (not including its descent). When you need to align it from the top you will run into problems.

One way to get around this (in GDI+) is to actually scan the glyph bitmap for the start of the top and then draw the glyph out with an offset that represent that result. Scan using BitmapLock and accessing the buffer directly.

You can of course also use Bitmap.GetPixel to do the scanning, but with a lot of text that will be a very slow process.

Update:

I forgot to mention something called bearings - in this case top side bearing which describes the gap from top of ascent to the top of the glyph. Unfortunately you cannot extract this via GDI/GDI+ (without writing a font parser). WPF however allow you to extract this information from a glyph.

Although not shown in this illustration, it shows the different parts of the glyph incl. side bearings which is the equivalent for the top:

bearings

For more information, see this link:
http://msdn.microsoft.com/en-us/library/system.windows.media.glyphtypeface.aspx

Perhaps this code can help. I wrote this in VB and translated to C# (so I hope nothing got lost in translation). This will take a glyph and return a bitmap of it with an exact bounding box for the glyph itself. This way just place the resulting bitmap at the vertical position you need:

It require a WPF typeface as argument (font opened with WPF instead of GDI+) - let me know if you need assistance with that:

using System.Windows.Media;
using System.Windows.Media.Imaging;

static class WPFGlyphToGDIPBitmap
{
    public static System.Drawing.Bitmap GetBitmapOfChar(GlyphTypeface gt, _
                                                        char c, _
                                                        double ptSize, _
                                                        float dpi)
    {

        ushort ci = 0;
        if (!gt.CharacterToGlyphMap.TryGetValue(Strings.AscW(c), ci)) {
            if (!gt.CharacterToGlyphMap.TryGetValue(Strings.Asc(c), ci))
                    return null;
        }

        Geometry geo = gt.GetGlyphOutline(ci, ptSize, ptSize);
        GeometryDrawing gDrawing = new GeometryDrawing(System.Windows.Media.Brushes.Black, null, geo);
        DrawingImage geoImage = new DrawingImage(gDrawing);
        geoImage.Freeze();

        DrawingVisual viz = new DrawingVisual();
        DrawingContext dc = viz.RenderOpen;

        dc.DrawImage(geoImage, new Rect(0, 0, geoImage.Width, geoImage.Height));
        dc.Close();

        RenderTargetBitmap bmp = new RenderTargetBitmap(geoImage.Width, _
                                                        geoImage.Height, _
                                                        dpi, dpi, _
                                                        PixelFormats.Pbgra32);
        bmp.Render(viz);

        PngBitmapEncoder enc = new PngBitmapEncoder();
        enc.Frames.Add(BitmapFrame.Create(bmp));

        MemoryStream ms = new MemoryStream();
        enc.Save(ms);
        ms.Seek(0, SeekOrigin.Begin);

        enc = null;
        dc = null;
        viz = null;

        DisposeBitmap(bmp);

        System.Drawing.Bitmap gdiBMP = new System.Drawing.Bitmap(ms);
        ms.Dispose();

        //gdiBMP.Save("c:\test.png", System.Drawing.Imaging.ImageFormat.Png)

        return gdiBMP;

    }

}
public static void DisposeBitmap(RenderTargetBitmap bmp)
{
    if (bmp != null) {
        bmp.Clear();
    }
    bmp = null;
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

Upvotes: 4

Related Questions