How to measure width of character precisely? - c#

Maybe I've got something wrong, but... I want to simulate character spacing.
I break the word (text) into the list of single characters, measure their widths, and then painting them one after another on the bitmap. I supposed, that overall width of the rendered text will be the same as the width of the whole not splitted string, but there is something wrong. Rendering characters in a loop show wider result. Is there any way to get common (expected) results?
here is a code snippet:
private struct CharWidths
{
public char Char;
public float Width;
}
private List<CharWidths> CharacterWidths = new List<CharWidths>();
...
private void GetCharacterWidths(string Text, Bitmap BMP)
{
int i;
int l = Text.Length;
CharacterWidths.Clear();
Graphics g = Graphics.FromImage(BMP);
CharWidths cw = new CharWidths();
for (i = 0; i < l; i++)
{
Size textSize = TextRenderer.MeasureText(Text[i].ToString(), Font);
cw.Char = Text[i];
cw.Width = textSize.Width;
CharacterWidths.Add(cw);
}
}
...
public void RenderToBitmap(Bitmap BMP)
{
//MessageBox.Show("color");
Graphics g = Graphics.FromImage(BMP);
GetCharacterWidths("Lyborko", BMP);
int i;
float X = 0;
PointF P = new PointF();
for (i = 0; i < CharacterWidths.Count; i++)
{
P.X = X;
P.Y = 0;
g.DrawString(CharacterWidths[i].Char.ToString(), Font, Brushes.White, P);
X = X+CharacterWidths[i].Width;
}
P.X = 0;
P.Y = 30;
g.DrawString("Lyborko", Font, Brushes.White, P);
// see the difference
}
Thanx a lot

First of all should say that don't have a silver bullet solution for this, but have a couple of suggessions on subject:
Considering that you by calling TextRenderer.MeasureText do not pass current device context (the same one you use to draw a string after) and knowing a simple fact that MeasureText simply in case of lack of that parameter creates a new one compatible with desktop and calls DrawTextEx WindowsSDK function, I would say first use an overload of MeasureText where you specify like a first argument device context which you use to render a text after. Could make a difference.
If it fails, I would try to use Control.GetPreferredSize method to guess most presize possible rendering dimension of the control on the screen, so actually the dimension of you future string's bitmap. To do that you can create some temporary control, assign a string, render and after call this function. It's clear to me that this solution may hardly fit in your app architecture, but can possibly produce a better results.
Hope this helps.

Related

Optimizing a bitmap character drawing algorithm in C#

Is there any way to optimize this:
A character is stored in a "matrix" of bytes, dimensions 9x16, for the sake of the example, let's call it character.
The bytes can be values either 1 or 0 , meaning draw foreground and draw background respectively.
The X and Y variables are integers, representing X and Y coordinates used for the SetPixel() function. BG and FG represent background and foreground colors respectively, both type of Color.
The drawing part of the algorithm itself looks like this:
for(int i=0;i<16;i++)
{
for(int j=0;j<9;j++)
{
if(character[i][j] == 1)
{
SetPixel(X,Y,BG);
}
else
{
SetPixel(X,Y,FG);
}
X++;
}
X=0;
Y++;
}
Later on, X incremented by 9 and Y is set back to 0.
The problem with this algorithm is , when it's called for drawing a string (many characters sequentially), it's extremely slow.
I'm not really sure what characters mean, however.
GetPixel internally calls LockBits to pin the memory
Ergo Its best to use LockBits once and be done with it
Always call UnlockBits
Direct pointer access using unsafe can give you a small amount of performance as well
Also (in this case) your for loops can be optimized (code wise) to include your other indexes.
Exmaple
protected unsafe void DoStuff(string path)
{
...
using (var b = new Bitmap(path))
{
var r = new Rectangle(Point.Empty, b.Size);
var data = b.LockBits(r, ImageLockMode.ReadOnly, PixelFormat.Format32bppPArgb);
var p = (int*)data.Scan0;
try
{
for (int i = 0; i < 16; i++, Y++)
for (int j = 0, X = 0; j < 9; j++, X++)
*(p + X + Y * r.Width) = character[i][j] == 1 ? BG : FG;
}
finally
{
b.UnlockBits(data);
}
}
}
Bitmap.LockBits
Locks a Bitmap into system memory.
Bitmap.UnlockBits
Unlocks this Bitmap from system memory.
unsafe
The unsafe keyword denotes an unsafe context, which is required for
any operation involving pointers.
Further reading
Unsafe Code and Pointers
Bitmap.GetPixel
LockBits vs Get Pixel Set Pixel - Performance

C# WinForms Chart Control: get Size,Region,Position of Bar

is there a way to get the rectangles of the stackcolumn chart bar?
this code snippet is how it can be works but it's very ugly:
var points = new List<Point>();
for (int x = 0; x < chart.Size.Width; x++)
{
for (int y = 0; y < chart.Size.Height; y++)
{
var hp = chart.HitTest(x, y, false, ChartElementType.DataPoint);
var result = hp.Where(h => h.Series?.Name == "Cats");
if (result.Count() > 0)
{
points.Add(new Point(x, y));
}
}
}
var bottomright = points.First();
var topleft = points.Last();
I will try to describe my purpose:
I would like to create a chart from various testresults and make this available as a HTML file. This generated Chart is inserted as an image file in the HTML document. Now, I would like to link each part of a Bar area from the Chart to an external document. Since the graphics is static, I have only the possibility to use the "MAP Area" element to make any area as a link from HTML. The "map" element requires a "rectangle", or these coordinates. That's the reason why I need the coordinator of each part of a Bar.
I have to mention that I am not really familiar with the Chart control yet.
The graphics is generated testweise.
[SOLVED]
i got the solution:
var stackedColumns = new List<Tuple<string,string,Rectangle>>();
for (int p = 0; p < chart.Series.Select(sm => sm.Points.Count).Max(); p++)
{
var totalPoints = 0;
foreach (var series in chart.Series)
{
var width = int.Parse(series.GetCustomProperty("PixelPointWidth"));
var x = (int)area.AxisX.ValueToPixelPosition(p + 1) - (width / 2);
int y = (int)area.AxisY.ValueToPixelPosition(totalPoints);
totalPoints += series.Points.Count > p ? (int)series.Points[p].YValues[0] : 0;
int y_total = (int)area.AxisY.ValueToPixelPosition(totalPoints);
var rect = new Rectangle(x, y_total, width, Math.Abs(y - y_total));
stackedColumns.Add(new Tuple<string, string, Rectangle>(series.Name, series.Points.ElementAtOrDefault(p)?.AxisLabel, rect));
}
}
this workaround works for stackedcolumn and points starts at x-axis=0.
just the PixelPointWidth property has to be set manualy to get the right width. i have not yet found a way to get the default bar width..
This is extremely tricky and I really wish I knew how to get the bounds from some chart functionionality!
You code snippet is actulally a good start for a workaround. I agree though that it has issues:
It is ugly
It doesn't always work
It has terrible performance
Let's tackle these issues one by one:
Yes it is ugly, but then that's the way of workarounds. My solution is even uglier ;-)
There are two things I found don't work:
You can't call a HitTest during a Pre/PostPaint event or terrible things will happen, like some Series go missing, SO exceptions or other crashes..
The result for the widths of the last Series are off by 1-2 pixels.
The performance of testing each pixel in the chart will be terrible even for small charts, but gets worse and worse when you enlarge the chart. This is relatively easy to prevent, though..:
What we are searching are bounding rectangles for each DataPoint of each Series.
A rectangle is defined by left and right or width plus top and bottom or height.
We can get precise values for top and bottom by using the axis function ValueToPixelPosition feeding in the y-value and 0 for each point. This is simple and cheap.
With that out of the way we still need to find the left and right edges of the points. To do so all we need to do it test along the zero-line. (All points will either start or end there!)
This greatly reduces the number of tests.
I have decided to do the testing for each series separately, restaring at 0 each time. For even better performance one could do it all in one go.
Here is a function that returns a List<Rectangle> for a given Series:
List<Rectangle> GetColumnSeriesRectangles(Series s, Chart chart, ChartArea ca)
{
ca.RecalculateAxesScale();
List<Rectangle> rex = new List<Rectangle>();
int loff = s == chart.Series.Last() ? 2 : 0; ;
int y0 = (int)ca.AxisY.ValueToPixelPosition(0);
int left = -1;
int right = -1;
foreach (var dp in s.Points)
{
left = -1;
int delta = 0;
int off = dp.YValues[0] > 0 ? delta : -delta;
for (int x = 0; x < chart.Width; x++)
{
var hitt = chart.HitTest(x, y0 +off );
if (hitt.ChartElementType == ChartElementType.DataPoint &&
((DataPoint)hitt.Object) == dp)
{
if (left < 0) left = x;
right = x;
}
else if (left > 0 && right > left) break;
}
int y = (int)ca.AxisY.ValueToPixelPosition(dp.YValues[0]);
rex.Add(new Rectangle(left, Math.Min(y0, y),
right - left + 1 - loff, Math.Abs(y - y0)));
left = -1;
}
return rex;
}
A few notes:
I start by doing a RecalculateAxesScale because we can't Hittest before the current layout has been calculated.
I use a helper variable loff to hold the offset for the width in the last Series.
I start searching at the last x coordinate as the points should all lie in sequence. If they don't because you have used funny x-values or inserted points you may need to start at 0 instead..
I use y0 as the baseline of the zero values for both the hittesting y and also the points' base.
I use a little Math to get the bounds right for both positive and negative y-values.
Here is a structure to hold those rectangles for all Series and code to collect them:
Dictionary<string, List<Rectangle>> ChartColumnRectangles = null;
Dictionary<string, List<Rectangle>> GetChartColumnRectangles(Chart chart, ChartArea ca)
{
Dictionary<string, List<Rectangle>> allrex = new Dictionary<string, List<Rectangle>>();
foreach (var s in chart.Series)
{
allrex.Add(s.Name, GetColumnSeriesRectangles(s, chart, ca));
}
return allrex;
}
We need to re-calculate the rectangles whenever we add points or resize the chart; also whenever the axis view changes. The common code for AxisViewChanged, ClientSizeChanged, Resize and any spot you add or remove points could look like this:
Chart chart= sender as Chart;
GetChartColumnRectangles(chart, chart.ChartAreas[0]);
Let's test the result with a Paint event:
private void chart1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
chart1.ApplyPaletteColors();
foreach (var kv in ChartColumnRectangles)
{
{
foreach (var r in kv.Value)
g.DrawRectangle(Pens.Black, r);
}
}
}
Here it is in action:
Well, I've been down this path and the BIG issue for me is that the custom property of 'PixelPointWidth' is just that - it is custom. You cannot retrieve it unless you've set it. I needed the width of the item - had to scwag/calculate it myself. Keep in mind that many charts can be panned/zoomed, so once you go down this path, then you need to recalculate it and set it for the chart prepaint events.
Here is a crude little function I made (is more verbose than needed - for educational purposes and has no error handling :)):
private int CalculateChartPixelPointWidth(Chart chart, ChartArea chartArea, Series series)
{
// Get right side - takes some goofy stuff - as the pixel location isn't available
var areaRightX = Math.Round(GetChartAreaRightPositionX(chart, chartArea));
var xValue = series.Points[0].XValue;
var xPixelValue = chartArea.AxisX.ValueToPixelPosition(xValue);
var seriesLeftX = chart.Location.X + xPixelValue;
var viewPointWidth = Math.Round((areaRightX - seriesLeftX - (series.Points.Count * 2)) / series.Points.Count, 0);
return Convert.ToInt32(viewPointWidth);
}
And this as well:
private double GetChartAreaRightPositionX(Chart chart, ChartArea area)
{
var xLoc = chart.Location.X;
return xLoc + (area.Position.Width + area.Position.X) / 100 * chart.Size.Width;
}
The reason I'm calculating this is because I need to draw some graphical overlays on top of the normal chart item objects (my own rendering for my own purposes).
In the 'prepaint' event for the chart, I need to calculate the 'PixelPointWidth' that matches the current chart view (might be panned/zoomed). I then use that value to SET the chart custom property to match . . . such that the normal chart entities and MINE are correctly aligned/scaled (ensures we're in exactly the right 'x' axis position):
In my prepaint event, I do the following - just prior to drawing my graphical entities:
// Pretty close scwag . . .
var viewPointWidth = CalculateChartPixelPointWidth(e.Chart, e.Chart.ChartAreas[0], e.Chart.Series[0]);
// Set the custom property and use the same point width for my own entities . .
chart1.Series[0].SetCustomProperty("PixelPointWidth", viewPointWidth.ToString("D"));
// . . . now draw my entities below . . .

How to get the part of string fitted in a rectangle layout by text renderer

I am trying to draw a string on a bitmap, but if the text is too long, part of the text may be clipped by text renderer. If I can find which part of text was written I can write the rest of it on another bitmap.
Is there any text rendering function in C# so it gets a text and a proposed layout rectangle and returns the amount of string written (fitted) in the layout rectangle?
string fittedString = TextRenderer.DrawText(graphics, text, rectangle);
If no, what is the easiest way to accomplish it?
Not one that I know of, but you can get an approximate by calling MeasureText() on each character of your string in a loop and sum up returned widths in a variable say W. When your sum exceeds the width of your target rectangle, you simply pick max value char height of that line in a separate variable say H. Upon reaching the end of each line you add the max value of height of current line in H.
You keep doing this until the value of H exceeds your target rectangle's height. That is simply the number of chars that fit in your rectangle.
Something like this (written by hand, plz adjust):
int HowManyChars(Graphics g, Font font, string text, Rectangle r)
{
float W=0, H=0, MaxH=0;
int i=0;
for(i=0; i< text.Length; i++)
{
var sz = g.MeasureString(text[i].ToString(), font);
W+= sz.Width;
if(W > r.Width)
{
W=sz.Width;
H+=MaxH;
MaxH = 0;
if(H>r.Height) break;
}
else
{
if(sz.Height > MaxH) MaxH = sz.Height;
}
}
return i;
}
TextRenderer.MeasureText(...) method returns a Size value which determines the size needed to draw a text. However, this method requires that the text is drawn on a single line. TextFormatFlags.WordBreak option makes it to wrap the text in a proposed layout.
We set the width of the proposed layout to the target rectangle width and in a loop measure the size of a substring of the main text rendered in this layout. if the size.Height returned by the method exceeds the height of our target rectangle, then we stop and return the index.
int HowManyChars(Font font, string text, Rectangle r)
{
int i = 0;
for (; i < text.Length; i++)
{
string str = text.Substring(0, i);
var size = TextRenderer.MeasureText(str, font,
new Size(r.Width, 0), TextFormatFlags.WordBreak);
if (size.Height > r.Height)
break;
}
return i;
}

Why doesn't c# bitmap setPixel() method work?

I have a method that takes in numbers for a rectangle and "blanks" the pixels (turns them white) for that area. For some reason though when my program searches for non white pixels, its finding the ones i just set to white.
Why is it saying its not white when it is? I have an image that gets cropped and is saved to my hard drive. So I'm able to view the area it says isn't white, but when i open the image, its completely white as can be. So I'm at a lose as to why this isn't working. The program stops on the same pixel every time. It says the R value is 238 and I know that pixel was set to white because i stepped through the debugger and watched the pixel value go into the bmp.SetPixel method.
This is the code for the "blanking" method:
void blankArea(int x, int y, int width, int height)
{
for (int i = y; i <= height+y; ++i)
for (int t = x; t <= width+x; ++t)
{
if (t == w)
break;
bmp.SetPixel(t, i, Color.White);
}
}
this is the code that says that pixel isn't white:
bool allWhiteColumn (int col)
{
for (int i = 5; i < h; ++i)
if (bmp.GetPixel(col - 1, i).R < 248 && bmp.GetPixel(col - 1, i).G < 248)
{
imageBelowColumEnd = bmp.GetPixel(col - 1, i).R;
this.row = i;
return false;
}
return true;
}
Any help at this point would be greatly appreciated. I have no idea why it's saying the R is 238 after I set it to white. Thanks
EDIT- since I post a comment I'll edit instead. Taking the if out, did nothing. Same problem. I can't check for plain white. The program finds images from a scanner. So because a scanners "white" areas are not true white, I can not look for Color.WHITE. I'll try the bitlock though...
EDIT 2- "Attempted to read or write protected memory. This is often an indication that other memory is corrupt." was thrown when i tried to do the example for the locking and unlocking of the bits. It threw it once it tried to copy the bits into the array. Apparently thats not allowed.
Thanks for the answers though.
You could try to use the Bitmap.Lockbits() method before, and Bitmap.Unlockbits() after you have changed the pixels.
Actually, here is an example: http://msdn.microsoft.com/en-us/library/5ey6h79d.aspx
Remove the if (t == w) break; in your first function, whatever is w (which you did not describe). I guess the code breaks and you end up with an incomplete white region.
Would it not make more sense to check for white for starters?
bool allWhiteColumn (int col)
{
for (int i = 5; i < h; ++i)
if (bmp.GetPixel(col - 1, i) != Color.White)
{
imageBelowColumEnd = bmp.GetPixel(col - 1, i).R;
this.row = i;
return false;
}
return true;
}

spring like drawing in c#

How to draw the spring like shape using c# drawing class
alt text http://img812.imageshack.us/img812/373/spring.jpg
First of all you'd need to think of a formula that would represent the spring. You could draw a circle and as you're going around it, let the X increase a bit. For instance:
for (double i = 0; i < 50; i += 0.01)
{
int x = (int)(Math.Sin(i) * 10 + i * 3);
int y =(int)(Math.Cos(i) * 10 + 50);
}
See the i variable there as time, and the result x and y the coordinates to draw; you'd traverse the path of the spring in small steps.
You could then create a new Bitmap and use the SetPixel method on those coordinates, and in the OnPaint method of your form, draw the bitmap on it.
If you're any good with math (I'm not :P) you might be able to only plot pixels inside the bitmap - the above example doesn't solve the problem of the minimum and maximum values for i.
This is more of a math problem than a C# one. What you want is to derive a Parametric equation for the curve you wish to draw.
With that go and fill an array of Point objects with values for the parametric equation on a certain interval with a certain step (the smaller the step the more the final drawing will look like the actual shape). Then you can use g.DrawLines (MSDN: DrawLines) to draw the actual curve on a surface.
You can edit the width, color and other properties of the line by modifying parameters of the Pen object.
Your actual code would look like this:
void DrawSpring (Graphics g)
{
List<Point> points = new List<Point>();
double step = 0.01;
for(double t = -2; t < 2; t += step)
{
Point p = new Point();
p.X = XPartOfTheEquation(t);
p.Y = YPartOfTheEquation(t);
points.Add(p);
}
g.DrawLines(new Pen(new SolidBrush(Color.Black), 2f), points.ToArray());
}

Categories

Resources