Dynamically resizing font to fit space while using Graphics.DrawString - c#

Does anyone have a tip whereas you could dynamically resize a font to fit a specific area? For example, I have an 800x110 rectangle and I want to fill it with the max size font that would support the entire string I'm trying to display.
Bitmap bitmap = new Bitmap(800, 110);
using (Graphics graphics = Graphics.FromImage(bitmap))
using (Font font1 = new Font("Arial", 120, FontStyle.Regular, GraphicsUnit.Pixel))
{
Rectangle rect1 = new Rectangle(0, 0, 800, 110);
StringFormat stringFormat = new StringFormat();
stringFormat.Alignment = StringAlignment.Center;
stringFormat.LineAlignment = StringAlignment.Center;
graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
graphics.DrawString("Billy Reallylonglastnameinstein", font1, Brushes.Red, rect1, stringFormat);
}
bitmap.Save(Server.MapPath("~/Fonts/" + System.Guid.NewGuid() + ".png"));
Obviously that whole name won't render in the space provided at the large font size. There has to be a simple way to do this?

You should do a scale transform on Font.Size the following function is an example of doing that but you can improve it to apply better results.
Here is FindFont function which get a room and a text with prefered size and gives you a font in which you can set that whole text fits the room!
// This function checks the room size and your text and appropriate font
// for your text to fit in room
// PreferedFont is the Font that you wish to apply
// Room is your space in which your text should be in.
// LongString is the string which it's bounds is more than room bounds.
private Font FindFont(
System.Drawing.Graphics g,
string longString,
Size Room,
Font PreferedFont
) {
// you should perform some scale functions!!!
SizeF RealSize = g.MeasureString(longString, PreferedFont);
float HeightScaleRatio = Room.Height / RealSize.Height;
float WidthScaleRatio = Room.Width / RealSize.Width;
float ScaleRatio = (HeightScaleRatio < WidthScaleRatio)
? ScaleRatio = HeightScaleRatio
: ScaleRatio = WidthScaleRatio;
float ScaleFontSize = PreferedFont.Size * ScaleRatio;
return new Font(PreferedFont.FontFamily, ScaleFontSize);
}
For your question you can call it like the following code:
Bitmap bitmap = new Bitmap(800, 110);
using (System.Drawing.Graphics graphics = System.Drawing.Graphics.FromImage(bitmap))
using (Font font1 = new Font("Arial", 120, FontStyle.Regular, GraphicsUnit.Pixel))
{
Rectangle rect1 = new Rectangle(0, 0, 800, 110);
StringFormat stringFormat = new StringFormat();
stringFormat.Alignment = StringAlignment.Center;
stringFormat.LineAlignment = StringAlignment.Center;
graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
Font goodFont = FindFont(graphics, "Billy Reallylonglastnameinstein", rect1.Size, font1);
graphics.DrawString(
"Billy Reallylonglastnameinstein",
goodFont,
Brushes.Red,
rect1,
stringFormat
);
}

I've adapted Saeed's great function to more suit my requirements. Comments explain all:
// You hand this the text that you need to fit inside some
// available room, and the font you'd like to use.
// If the text fits nothing changes
// If the text does not fit then it is reduced in size to
// make it fit.
// PreferedFont is the Font that you wish to apply
// FontUnit is there because the default font unit is not
// always the one you use, and it is info required in the
// constructor for the new Font.
public static void FindGoodFont(Graphics Graf, string sStringToFit,
Size TextRoomAvail,
ref Font FontToUse,
GraphicsUnit FontUnit)
{
// Find out what the current size of the string in this font is
SizeF RealSize = Graf.MeasureString(sStringToFit, FontToUse);
Debug.WriteLine("big string is {0}, orig size = {1},{2}",
sStringToFit, RealSize.Width, RealSize.Height);
if ((RealSize.Width <= TextRoomAvail.Width) && (RealSize.Height <= TextRoomAvail.Height))
{
Debug.WriteLine("The space is big enough already");
// The current font is fine...
return;
}
// Either width or height is too big...
// Usually either the height ratio or the width ratio
// will be less than 1. Work them out...
float HeightScaleRatio = TextRoomAvail.Height / RealSize.Height;
float WidthScaleRatio = TextRoomAvail.Width / RealSize.Width;
// We'll scale the font by the one which is furthest out of range...
float ScaleRatio = (HeightScaleRatio < WidthScaleRatio) ? ScaleRatio = HeightScaleRatio : ScaleRatio = WidthScaleRatio;
float ScaleFontSize = FontToUse.Size * ScaleRatio;
Debug.WriteLine("Resizing with scales {0},{1} chose {2}",
HeightScaleRatio, WidthScaleRatio, ScaleRatio);
Debug.WriteLine("Old font size was {0}, new={1} ",FontToUse.Size,ScaleFontSize);
// Retain whatever the style was in the old font...
FontStyle OldFontStyle = FontToUse.Style;
// Get rid of the old non working font...
FontToUse.Dispose();
// Tell the caller to use this newer smaller font.
FontToUse = new Font(FontToUse.FontFamily,
ScaleFontSize,
OldFontStyle,
FontUnit);
}

This is just an update for #Saeed's FindFont function.
GraphicsUnit.Pixel needs to be added to FindFont function return line. Without GraphicsUnit.Pixel, system dpi will effect the drawn string. Problem will arise when the dpi of system and bitmap mismatches. You can see more detail in this Windows DPI setting affects Graphics.DrawString. Since GraphicsUnit of PreferedFont is already set to GraphicsUnit.Pixel and return font is not set with GraphicsUnit.Pixel. In this case text will go out of the Room dimension, if bitmap dpi is larger than system dpi and font size will go smaller than the expected size if bitmap dpi is smaller than system dpi. Here is the updated function.
private Font FindFont( System.Drawing.Graphics g , string longString , Size Room , Font PreferedFont)
{
SizeF RealSize = g.MeasureString(longString, PreferedFont);
float HeightScaleRatio = Room.Height / RealSize.Height;
float WidthScaleRatio = Room.Width / RealSize.Width;
float ScaleRatio = (HeightScaleRatio < WidthScaleRatio) ? ScaleRatio = HeightScaleRatio : ScaleRatio = WidthScaleRatio;
float ScaleFontSize = PreferedFont.Size * ScaleRatio;
return new Font(PreferedFont.FontFamily, ScaleFontSize,PreferedFont.Style,GraphicsUnit.Pixel);
}

I don't want to bash against saaeds solution which is probably pretty awesome, too. But I found another one on msdn: Dynamic Graphic Text Resizing which worked for me.
public Font GetAdjustedFont(Graphics GraphicRef, string GraphicString, Font OriginalFont, int ContainerWidth, int MaxFontSize, int MinFontSize, bool SmallestOnFail)
{
// We utilize MeasureString which we get via a control instance
for (int AdjustedSize = MaxFontSize; AdjustedSize >= MinFontSize; AdjustedSize--)
{
Font TestFont = new Font(OriginalFont.Name, AdjustedSize, OriginalFont.Style);
// Test the string with the new size
SizeF AdjustedSizeNew = GraphicRef.MeasureString(GraphicString, TestFont);
if (ContainerWidth > Convert.ToInt32(AdjustedSizeNew.Width))
{
// Good font, return it
return TestFont;
}
}
// If you get here there was no fontsize that worked
// return MinimumSize or Original?
if (SmallestOnFail)
{
return new Font(OriginalFont.Name,MinFontSize,OriginalFont.Style);
}
else
{
return OriginalFont;
}
}

Here is my solution that support wrapping.
public static Font GetAdjustedFont(Graphics graphic, string str, Font originalFont, Size containerSize)
{
// We utilize MeasureString which we get via a control instance
for (int adjustedSize = (int)originalFont.Size; adjustedSize >= 1; adjustedSize--)
{
var testFont = new Font(originalFont.Name, adjustedSize, originalFont.Style, GraphicsUnit.Pixel);
// Test the string with the new size
var adjustedSizeNew = graphic.MeasureString(str, testFont, containerSize.Width);
if (containerSize.Height > Convert.ToInt32(adjustedSizeNew.Height))
{
// Good font, return it
return testFont;
}
}
return new Font(originalFont.Name, 1, originalFont.Style, GraphicsUnit.Pixel);
}
How to use:
var font = GetAdjustedFont(drawing, text, originalfont, wrapSize);
drawing.DrawString(text, font, textBrush, new Rectangle(0, 0, wrapSize.Width, wrapSize.Height));

Related

C# DrawString with StringFormatFlags: How to Flip Vertical Orientation?

I'm drawing strings on an image and saving it as a TIFF. I need to orient the text like this:
desired output
I'm using this code to create the string format:
formatFlags = (StringFormatFlags.NoClip | StringFormatFlags.DirectionVertical | StringFormatFlags.DirectionRightToLeft);
And this is the output:
actual output
How can I simply flip the orientation around?
Update: Based on the comment, I tried this, but it is place my text way up in the corner instead of where it would be if I didn't add in that sample code. I commented out my original DrawString.
protected virtual void AddTextToBackground(Graphics backgroundGfx, FeatureLocation featureLocation, TextFeature textFeature, int equator) {
Font font = CreateFont(textFeature);
Color fontColor = ColorTranslator.FromHtml(textFeature.FontColor);
Brush textBrush = new SolidBrush(fontColor);
// Determine postion of text box
int xPos = featureLocation.XPos - TEXT_RECT_WIDTH / 2;
int adjustedEquatorOffset = CalculateTextEquatorOffset(featureLocation.EquatorOffset);
//int adjustedEquatorOffset = featureLocation.EquatorOffset;
int yPos = CalculateYPos(equator, adjustedEquatorOffset, TEXT_RECT_WIDTH);
// Rectangle is necessary to create centered text using StringFormat in the DrawString call
Rectangle rect = new Rectangle(xPos, yPos, TEXT_RECT_WIDTH, TEXT_RECT_WIDTH);
// Set up alignment
StringFormat stringFormat = new StringFormat {
Alignment = StringAlignment.Center,
LineAlignment = FindStringAlignment(featureLocation.EquatorOffset)
};
// Determine rotation and set format flags
stringFormat.FormatFlags = GetTextFormatFlags(featureLocation.DefaultRotation, textFeature.Rotation);
// Draw text
SizeF sz = backgroundGfx.VisibleClipBounds.Size;
backgroundGfx.TranslateTransform(sz.Width / 2, sz.Height / 2);
backgroundGfx.RotateTransform(45);
//backgroundGfx.DrawString(textFeature.Text, font, textBrush, rect, stringFormat);
backgroundGfx.DrawString(textFeature.Text, font, textBrush, -(sz.Width/2), -(sz.Height/2), stringFormat);
backgroundGfx.ResetTransform();
}

Issues with DrawString on a Bitmap

I have this constructor that takes in an input of an array of Color objects and some misc strings (like file location and size of list), and stores this heatmap into a bitmap instance called _image_. Everything works out fine, but my main issue is I have no way to render text onto this heatmap. I would like to overlay title text and x and y-axis labels onto this bitmap.
public HeatMap(IEnumerable<Color> colors, int width, int height, string file, int U, int V) {
if (colors == null)
throw new ArgumentNullException("colors");
if (width <= 0)
throw new ArgumentException("width must be at least 1");
if (height <= 0)
throw new ArgumentException("height must be at least 1");
_width = width;
_height = height;
_file = file;
_image = new Bitmap(U, V, PixelFormat.Format32bppArgb);
Graphics graphics = Graphics.FromImage(_image);
graphics.Clear(Color.White);
graphics.Dispose();
int x = 0;
int y = 0;
foreach (Color color in colors) {
_image.SetPixel(x, y, color); // This can be speeded up by using GH_MemoryBitmap if you want.
y++;
if (y >= V) {
y = 0;
x++;
}
if (x >= U)
break;
}
}
Below, I also have a method that resizes the image (without too much distortion), and I figured I might as well make use of this since I have a graphics object I can use, like so:
private Bitmap ResizeBitmap(Bitmap sourceBMP, int width, int height) {
Bitmap result = new Bitmap(width, height);
using (Graphics g = Graphics.FromImage(result)) {
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
g.DrawImage(sourceBMP, 0, 0, width, height);
g.DrawString("A heatmap.", SystemFonts.DefaultFont, Brushes.Black, 10, 10);
}
return result;
}
Using the above, I was able to overlay text over it, as a starting point. My question is, how do I add a white boarder over the above image (so my text doesn't overlap my graph), and then overlay x and y-axis text onto it?
I guess my specific question really is - how do I render text onto, say, every four columns of my heatmap? In most instances, there are 100 x-axis objects, and like 24 y-axis objects. The y-axis would have the time of the day, while the x-axis has the day of the year. My familiarity with using graphics in C# is very low, so any pointers is very appreciated.
You've to use the DrawString before the graphics.Dispose() and after the Clear().
Graphics graphics = Graphics.FromImage(_image);
graphics.Clear(Color.WhiteSmoke);
graphics.DrawString("your text", SystemFonts.DefaultFont, Brushes.Black, new PointF(0, 0));
graphics.Dispose();
Where the PointF(0, 0) represents the up-left corner of your text placeholder, you've to calculate the text position based on the positions of your heatmap.
UPDATE (alignment)
You can align your text in relation with a virtual rectangle, doing something like:
Graphics graphics = Graphics.FromImage(_image);
graphics.Clear(Color.WhiteSmoke);
StringFormat stringFormat = new StringFormat() { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
graphics.DrawString("your text", SystemFonts.DefaultFont, Brushes.Black, new RectangleF(0, 0, 100, 100), stringFormat);
graphics.Dispose();
In this case I've created a virtual box of 100x100 positioned on the top-left corner, and the text is h/v centered relatively to this box.

C# adding string to image, using the max font size

I want to add 2 string to an image as follows:
This is Text
------------
------------
------------
--Other-----
How do I use the largest font size possible without have the String go off the side of the image?
example: if text is too big then it goes off the image::
This is Text That is too big
------------
------------
------------
--Other-----
I wrote this script on my previous projects to fit some text into the image by calculating its dimensions for each font-size. when the font size is bigger than the image's width, it lowers the font size by 0.1em and tries again until the text fits in the image. Here's the code :
public static string drawTextOnMarker(string markerfile, string text, string newfilename,Color textColor)
{
//Uri uri = new Uri(markerfile, UriKind.Relative);
//markerfile = uri.AbsolutePath;
//uri = new Uri(newfilename, UriKind.Relative);
//newfilename = uri.AbsolutePath;
if (!System.IO.File.Exists(System.Web.HttpContext.Current.Server.MapPath(newfilename)))
{
try
{
Bitmap bmp = new Bitmap(System.Web.HttpContext.Current.Server.MapPath(markerfile));
Graphics g = Graphics.FromImage(bmp);
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
StringFormat strFormat = new StringFormat();
strFormat.Alignment = StringAlignment.Center;
SolidBrush myBrush = new SolidBrush(textColor);
float fontsize = 10;
bool sizeSetupCompleted = false;
while (!sizeSetupCompleted)
{
SizeF mySize = g.MeasureString(text, new Font("Verdana", fontsize, FontStyle.Bold));
if (mySize.Width > 24 || mySize.Height > 13)
{
fontsize-= float.Parse("0.1");
}
else
{
sizeSetupCompleted = true;
}
}
g.DrawString(text, new Font("Verdana", fontsize, FontStyle.Bold), myBrush, new RectangleF(4, 3, 24, 8), strFormat);
bmp.Save(System.Web.HttpContext.Current.Server.MapPath(newfilename));
return newfilename.Substring(2);
}
catch (Exception)
{
return markerfile.Substring(2);
}
}
return newfilename.Substring(2);
}
Here is a quick solution:
using (Graphics g = Graphics.FromImage(bmp))
{
float width = g.MeasureString(text, font).Width;
float scale = bmp.Width / width;
g.ScaleTransform(scale, scale); //Simple trick not to use other Font instance
g.DrawString(text, font, Brushes.Black, PointF.Empty);
g.ResetTransform();
...
}
Your text won't be always 100% width if you use TextRenderingHint.AntiAliasGridFit or similar, but I think that's not a problem as you just want to make sure the text alawys fits in the image.

Calculating the minimum width needed to show text in X lines, in .Net?

How could you calculate the minimum width needed to display a string in X lines, given that text should break on whitespace?
Possible Hint: Perhaps some sort of binary search using Graphics.MeasureString()?
Edit: Didn't realize you wanted to try and fit the text to a fixed number of lines. This was a tough one to try and solve. This is the best I could come up with and may not be the most elegant, but it seems to work:
public SizeF CalculateWidth(Font font, Graphics graphics, int numOfLines,
string text)
{
SizeF sizeFull = graphics.MeasureString(text, font,
new SizeF(
float.PositiveInfinity,
float.PositiveInfinity),
StringFormat.
GenericTypographic);
float width = sizeFull.Width/numOfLines;
float averageWidth = sizeFull.Width/text.Length;
int charsFitted;
int linesFilled;
SizeF needed = graphics.MeasureString(text, font,
new SizeF(width,
float.
PositiveInfinity),
StringFormat.
GenericTypographic,
out charsFitted,
out linesFilled);
while (linesFilled > numOfLines)
{
width += averageWidth;
needed = graphics.MeasureString(text, font,
new SizeF(width,
float.PositiveInfinity),
StringFormat.GenericTypographic,
out charsFitted, out linesFilled);
}
return needed;
}
Example usage:
Font font = new Font("Arial", 12, FontStyle.Regular,
GraphicsUnit.Pixel);
Graphics g = Graphics.FromImage(new Bitmap(1, 1));
string text = "Some random text with words in it.";
SizeF size = CalculateWidth(font, g, 3, text);

Center text output from Graphics.DrawString()

I'm using the .NETCF (Windows Mobile) Graphics class and the DrawString() method to render a single character to the screen.
The problem is that I can't seem to get it centred properly. No matter what I set for the Y coordinate of the location of the string render, it always comes out lower than that and the larger the text size the greater the Y offset.
For example, at text size 12, the offset is about 4, but at 32 the offset is about 10.
I want the character to vertically take up most of the rectangle it's being drawn in and be centred horizontally. Here's my basic code. this is referencing the user control it's being drawn in.
Graphics g = this.CreateGraphics();
float padx = ((float)this.Size.Width) * (0.05F);
float pady = ((float)this.Size.Height) * (0.05F);
float width = ((float)this.Size.Width) - 2 * padx;
float height = ((float)this.Size.Height) - 2 * pady;
float emSize = height;
g.DrawString(letter, new Font(FontFamily.GenericSansSerif, emSize, FontStyle.Regular),
new SolidBrush(Color.Black), padx, pady);
Yes, I know there is the label control that I could use instead and set the centring with that, but I actually do need to do this manually with the Graphics class.
I'd like to add another vote for the StringFormat object.
You can use this simply to specify "center, center" and the text will be drawn centrally in the rectangle or points provided:
StringFormat format = new StringFormat();
format.LineAlignment = StringAlignment.Center;
format.Alignment = StringAlignment.Center;
However there is one issue with this in CF. If you use Center for both values then it turns TextWrapping off. No idea why this happens, it appears to be a bug with the CF.
To align a text use the following:
StringFormat sf = new StringFormat();
sf.LineAlignment = StringAlignment.Center;
sf.Alignment = StringAlignment.Center;
e.Graphics.DrawString("My String", this.Font, Brushes.Black, ClientRectangle, sf);
Please note that the text here is aligned in the given bounds. In this sample this is the ClientRectangle.
Through a combination of the suggestions I got, I came up with this:
private void DrawLetter()
{
Graphics g = this.CreateGraphics();
float width = ((float)this.ClientRectangle.Width);
float height = ((float)this.ClientRectangle.Width);
float emSize = height;
Font font = new Font(FontFamily.GenericSansSerif, emSize, FontStyle.Regular);
font = FindBestFitFont(g, letter.ToString(), font, this.ClientRectangle.Size);
SizeF size = g.MeasureString(letter.ToString(), font);
g.DrawString(letter, font, new SolidBrush(Color.Black), (width-size.Width)/2, 0);
}
private Font FindBestFitFont(Graphics g, String text, Font font, Size proposedSize)
{
// Compute actual size, shrink if needed
while (true)
{
SizeF size = g.MeasureString(text, font);
// It fits, back out
if (size.Height <= proposedSize.Height &&
size.Width <= proposedSize.Width) { return font; }
// Try a smaller font (90% of old size)
Font oldFont = font;
font = new Font(font.Name, (float)(font.Size * .9), font.Style);
oldFont.Dispose();
}
}
So far, this works flawlessly.
The only thing I would change is to move the FindBestFitFont() call to the OnResize() event so that I'm not calling it every time I draw a letter. It only needs to be called when the control size changes. I just included it in the function for completeness.
To draw a centered text:
TextRenderer.DrawText(g, "my text", Font, Bounds, ForeColor, BackColor,
TextFormatFlags.HorizontalCenter |
TextFormatFlags.VerticalCenter |
TextFormatFlags.GlyphOverhangPadding);
Determining optimal font size to fill an area is a bit more difficult. One working soultion I found is trial-and-error: start with a big font, then repeatedly measure the string and shrink the font until it fits.
Font FindBestFitFont(Graphics g, String text, Font font,
Size proposedSize, TextFormatFlags flags)
{
// Compute actual size, shrink if needed
while (true)
{
Size size = TextRenderer.MeasureText(g, text, font, proposedSize, flags);
// It fits, back out
if ( size.Height <= proposedSize.Height &&
size.Width <= proposedSize.Width) { return font; }
// Try a smaller font (90% of old size)
Font oldFont = font;
font = new Font(font.FontFamily, (float)(font.Size * .9));
oldFont.Dispose();
}
}
You'd use this as:
Font bestFitFont = FindBestFitFont(g, text, someBigFont, sizeToFitIn, flags);
// Then do your drawing using the bestFitFont
// Don't forget to dispose the font (if/when needed)
Here's some code. This assumes you are doing this on a form, or a UserControl.
Graphics g = this.CreateGraphics();
SizeF size = g.MeasureString("string to measure");
int nLeft = Convert.ToInt32((this.ClientRectangle.Width / 2) - (size.Width / 2));
int nTop = Convert.ToInt32((this.ClientRectangle.Height / 2) - (size.Height / 2));
From your post, it sounds like the ClientRectangle part (as in, you're not using it) is what's giving you difficulty.
You can use an instance of the StringFormat object passed into the DrawString method to center the text.
See Graphics.DrawString Method and StringFormat Class.

Categories