Search code examples
c#.netimagegraphicsmeasurestring

Custom Measure String in C# without Graphics.MeasureString


I am trying to create an Image with a Caption/Text on it for Youtube-Thumbnails.

Following Rules are defined:

  • The Text is the Title of the Video and always changes from Thumbnail to Thumbnail.
  • The Porgram uses a pre-defined Text-Width which must not be touched by the Text on the Image.
  • The Text should be as close to the pre-defined with as possible.

So my thoughts were that I would use Graphics.MeasureString to be able to track the Width of the String on the Image and increase the Font-Size and repeat this until the pre-defined Width is closely reached but not touched.

But I have tested it with MeasureString and found out that it isn't that accurate. And also found confirmation here: Graphics.MeasureCharacterRanges giving wrong size calculations

I have tried the things they have recommended but with no success as the final width of my string always overflooded the image borders. Even if my pre-defined Width was way smaller than the Image Width. (Image Width: 1920; Pre-Defined Width: 1600)

So I came up with the Idea to create a custom Measurement Method and to write the String as I want it on a new Bitmap and to count the maximum Pixels of the String in Height and Width. (The Height is just for future stuff)

My current Code is:

public static SizeF MeasuredStringSize(string text, Bitmap originBitmap, FontFamily fontFamily, StringFormat strformat)
{
    int currentFontSize = 10;
    SizeF measuredSize = new();

    var highestWidth = 0;
    var highestHeight = 0;



    while (highestWidth < maximumTextWidth)
    {

        Bitmap bitmap = new(originBitmap);
        Bitmap _bitmap = new(bitmap.Width, bitmap.Height);
        using Graphics graphics = Graphics.FromImage(bitmap);

        if (graphics != null)
        {
            graphics.TranslateTransform(bitmap.Width / 2, bitmap.Height / 2);

            currentFontSize++;
            graphics.Clear(Color.White);

            using GraphicsPath path = new();
            using SolidBrush brush = new(Color.Red);
            using Pen pen = new(Color.Red, 6)
            {
                LineJoin = LineJoin.Round
            };

            path.AddString(text, fontFamily, (int)fontStyle, currentFontSize, new Point(0, 0), strformat);
            graphics.DrawPath(pen, path);
            graphics.FillPath(brush, path);

            Dictionary<int, List<int>> redPixelMatrix = new();

            for (int i = 0; i < bitmap.Width; i++)
            {
                for (int j = 0; j < bitmap.Height; j++)
                {
                    var currentPixelColor = bitmap.GetPixel(i, j);

                    if (currentPixelColor.B != 255 && currentPixelColor.G != 255 && currentPixelColor.R == 255)
                    {
                        if (!redPixelMatrix.ContainsKey(i))
                        {
                            redPixelMatrix.Add(i, new());
                        }

                        redPixelMatrix[i].Add(j);
                    }
                }
            }

            highestWidth = redPixelMatrix.Keys.Count;
            highestHeight = redPixelMatrix.Aggregate((l, r) => l.Value.Count > r.Value.Count ? l : r).Value.Count;

            Console.WriteLine($"X:{highestWidth};Y:{highestHeight}");

            //Debugging the final Image with Text to see the Result
            bitmap.Save(ResultPath);

        }
    }
    
    measuredSize = new SizeF(highestWidth, highestHeight);
    return measuredSize;
}

The Resulting Image from bitmap.Save(ResultPath); as the String reaches the Image borders looks like this: enter image description here

But the exact String width is 1742 instead of the width of my Image 1920 which should be more or less the same at this moment.

So, why is the Text nearly as wide as the Image but doesn't have the same width?


Solution

  • highestWidth = redPixelMatrix.Keys.Count; This will just count the number of columns containing red pixels, excluding any spaces in the text. You presumably want the minimum and maximum indices.

    I.e.

    var minX = int.MaxValue;
    var maxX = int.MinValue;
    // Loops over rows & columns
        // Check if pixel is red
            if(i > maxX) maxX = i;
            if(i <  minX) minX = i;
    
    

    If you only want the text width and not the bounds you can just do maxX - minX.