Color value with alpha of zero shows up as black - gdi+

I'm using .NET 4.0. I don't know if this is a framework bug or if it's a GDI+ thing. I just discovered it while writing an app to swap color channels.
Let me try to explain the problem. I'm reading pixels from one bitmap, swapping the channels, and writing them out to another bitmap. (Specifically, I'm setting the output image's RGB values equal to the input image's alpha, and output's alpha equal to the input's green channel… or, to put it succinctly, A => RGB and G => A.) The code is as follows:
for (int y = 0; y < input.Height; y++)
{
for (int x = 0; x < input.Width; x++)
{
Color srcPixel = input.GetPixel(x, y);
int alpha = srcPixel.A;
int green = srcPixel.G;
Color destPixel = Color.FromArgb(green, alpha, alpha, alpha);
output.SetPixel(x, y, destPixel);
}
}
Similarly, I've tried this:
int color = green << 24 | alpha << 16 | alpha << 8 | alpha;
Color destPixel = Color.FromArgb(color);
output.SetPixel(x, y, destPixel);
For the most part, it works.
The problem: regardless of what the RGB values are, when alpha is zero, the resultant RGB value is always pure black (R:0, G:0, B:0). I don't know if this is some sort of FromArgb() "optimization" — using .NET Reflector, I don't see FromArgb() doing anything strange — or if Bitmap.SetPixel is the culprit — more likely since it defers to native code and I can't look at it. Either way, when alpha is zero, the pixel is black. This is not the behavior I expected. I need to keep RGB channels intact.
At first I thought it was a pre-multiplied alpha issue, because I'm loading DDS files using my home-brewed DDS loader (which I built to spec and which has never given me any issues), but when I specify an explicit alpha of 255, like this:
Color destPixel = Color.FromArgb(255, alpha, alpha, alpha);
...the RGB channels show up correctly — i.e., none of them turns out black — so it's definitely something within GDI+ that erroneously assumes RGB values can be safely ignored if the alpha is zero… which, to me, seems like a pretty stupid assumption, but, whatever.
Further exacerbating the problem is that the Color type is immutable, which makes sense for a structure, but it means I can't create a color and then assign the alpha… which, if SetPixel() is the culprit, wouldn't matter anyway. (I've tested this by geting the pixel again immediately after setting it and seeing the same results: zero alpha = zero RGB).
So, my question: has anyone dealt with this issue and come up with a relatively simple workaround? In an effort to keep my dependencies down, I am loathe to import a third-party image library, but since GDI+ is making buggy assumptions about my color channels, I may not have a choice.
Thanks for your help.
EDIT: I solved this, but I can't post the answer for another seven hours. Awesome.

Sorry for the delay. Anyway, I should have worked on this a bit longer before posting, because I found a solution about five or ten minutes later. To be clear, I didn't find a solution to the stated GDI+ issue, but I found a suitable workaround. I thought about how, in other API's, I would lock a surface and transfer bytes directly to another surface, so I took that approach. After a little help from MSDN, here's my code (sans error handling):
Bitmap input = Bitmap.FromFile(filename) as Bitmap;
int byteCount = input.Width * input.Height * 4;
var inBytes = new byte[byteCount];
var outBytes = new byte[byteCount];
var inBmpData = input.LockBits(new Rectangle(0, 0, input.Width, input.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(inBmpData.Scan0, inBytes, 0, byteCount);
for (int y = 0; y < input.Height; y++)
{
for (int x = 0; x < input.Width; x++)
{
int offset = (input.Width * y + x) * 4;
// byte blue = inBytes[offset];
byte green = inBytes[offset + 1];
// byte red = inBytes[offset + 2];
byte alpha = inBytes[offset + 3];
outBytes[offset] = alpha;
outBytes[offset + 1] = alpha;
outBytes[offset + 2] = alpha;
outBytes[offset + 3] = green;
}
}
input.UnlockBits(inBmpData);
Bitmap output = new Bitmap(input.Width, input.Height, PixelFormat.Format32bppArgb);
var outBmpData = output.LockBits(new Rectangle(0, 0, output.Width, output.Height), ImageLockMode.WriteOnly, output.PixelFormat);
Marshal.Copy(outBytes, 0, outBmpData.Scan0, outBytes.Length);
output.UnlockBits(outBmpData);
Notes: Marshal is under System.Runtime.InteropServices; BitmapData (inBmpData, outBmpData), ImageLockMode, and PixelFormat are under System.Drawing.Imaging.
Not only does this work perfectly, but it is phenomenally faster. I'll be using this technique from now on for all my channel swapping needs. (I've already used it in another, similar app.)
Sorry for the needless post. I at least hope this solution helps someone else.

Related

CS50, Pset 4, filter-less, reflection issues

so im currently trying to do the less comfortable pset 4 and have just got to the reflection part.
My code seems to work with the examples provided but fails the CS50 Check, I believe its something to do with my logic that i just cant get my head around. Maybe to do with the fact ive not actually done anything regarding whether the width is even or odd.
Also I originally tried to put the * pointer on the "image[i][j]" parts but that didnt work so then decided to do it on the "temp" part and discovered I had to use "malloc". Im not sure why this is the case as in the lecture its the variables we want changing and not the temporary variable that was assigned a pointer, any explanation towards that would also be appreciated.
I've also never used this site before so apologies if this doesnt make sense.
PG x
edit: I've also just realise it 'works' on the images provided even if there are no pointers.
edit: I saw on another question that doing "image[i][width -1 - j]" works, so now it passes the check but im not sure why its a "-1", is it because arrays start at [0] ?
void reflect(int height, int width, RGBTRIPLE image[height][width])
for (int i = 0; i < height; i++)
{
//width / 2 as we swap half the picture with the other half (unless its an uneven width?! :O)
for (int j = 0; j < width / 2; j++)
{
//first pixel = last pixel
//the * means that we are going to the LOCATION of temp in memory rather than just the value itself
RGBTRIPLE *temp = malloc(sizeof(RGBTRIPLE));
*temp = image[i][j];
image[i][j] = image[i][width -j];
image[i][width - j] = *temp;
free(temp);
}
}

Usage of Map and Translate Functions in Processing

New to Processing working on understanding this code:
import com.onformative.leap.LeapMotionP5;
import java.util.*;
LeapMotionP5 leap;
LinkedList<Integer> values;
public void setup() {
size(800, 300);
frameRate(120); //Specifies the number of frames to be displayed every second
leap = new LeapMotionP5(this);
values = new LinkedList<Integer>();
stroke(255);
}
int lastY = 0;
public void draw() {
**translate(0, 180)**; //(x, y, z)
background(0);
if (values.size() >= width) {
values.removeFirst();
}
values.add((int) leap.getVelocity(leap.getHand(0)).y);
System.out.println((int) leap.getVelocity(leap.getHand(0)).y);
int counter = 0;
** for (Integer val : values)** {
**val = (int) map(val, 0, 1500, 0, height);**
line(counter, val, counter - 1, lastY);
point(counter, val);
lastY = val;
counter++;
}
** line(0, map(1300, 0, 1500, 0, height), width, map(1300, 0, 1500, 0, height)); //(x1, y1, x2, y2)**
}
It basically draw of graph of movement detected on the y axis using the Leap Motion sensor. Output looks like this:
I eventually need to do something similar to this that would detect amplitude instead of velocity simultaneously on all 3 axis instead of just the y.
The use of Map and Translate are whats really confusing me. I've read the definitions of these functions on the Processing website so I know what they are and the syntax, but what I dont understand is the why?! (which is arguably the most important part.
I am asking if someone can provide simple examples that explain the WHY behind using these 2 functions. For instance, given a program that needs to do A, B, and C, with data foo, y, and x, you would use Map or Translate because A, B, and C.
I think programming guides often overlook this important fact but to me it is very important to truly understanding a function.
Bonus points for explaining:
for (Integer val : values) and LinkedList<Integer> values; (cant find any documentation on the processing website for these)
Thanks!
First, we'll do the easiest one. LinkedList is a data structure similar to ArrayList, which you may be more familiar with. If not, then it's just a list of values (of the type between the angle braces, in this case integer) that you can insert and remove from. It's a bit complicated on the inside, but if it doesn't appear in the Processing documentation, it's a safe bet that it's built into Java itself (java documentation).
This line:
for (Integer val : values)
is called a "for-each" or "foreach" loop, which has plenty of very good explanation on the internet, but I'll give a brief explanation here. If you have some list (perhaps a LinkedList, perhaps an ArrayList, whatever) and want to do something with all the elements, you might do something like this:
for(int i = 0; i < values.size(); i++){
println(values.get(i)); //or whatever
println(values.get(i) * 2);
println(pow(values.get(i),3) - 2*pow(values.get(i),2) + values.get(i));
}
If you're doing a lot of manipulation with each element, it quickly gets tedious to write out values.get(i) each time. The solution would be to capture values.get(i) into some variable at the start of the loop and use that everywhere instead. However, this is not 100% elegant, so java has a built-in way to do this, which is the for-each loop. The code
for (Integer val : values){
//use val
}
is equivalent to
for(int i = 0; i < values.size(); i++){
int val = values.get(i);
//use val
}
Hopefully that makes sense.
map() takes a number in one linear system and maps it onto another linear system. Imagine if I were an evil professor and wanted to give students random grades from 0 to 100. I have a function that returns a random decimal between 0 and 1, so I can now do map(rand(),0,1,0,100); and it will convert the number for me! In this example, you could also just multiply by 100 and get the same result, but it is usually not so trivial. In this case, you have a sensor reading between 0 and 1500, but if you just plotted that value directly, sometimes it would go off the screen! So you have to scale it to an appropriate scale, which is what that does. 1500 is the max that the reading can be, and presumably we want the maximum graphing height to be at the edge of the screen.
I'm not familiar with your setup, but it looks like the readings can be negative, which means that they might get graphed off the screen, too. The better solution would be to map the readings from -1500,1500 to 0,height, but it looks like they chose to do it a different way. Whenever you call a drawing function in processing (eg point(x,y)), it draws the pixels at (x,y) offset from (0,0). Sometimes you don't want it to draw it relative to (0,0), so the translate() function allows you to change what it draws things relative against. In this case, translating allows you to plot some point (x,0) somewhere in the middle of the screen, rather than on the edge.
Hope that helps!

Why does this code make the QImage lose its alpha channel?

I'm trying to understand why the code below changes the QImage in Qt. It's not meant to do anything (yet), it's just for testing. When I run the code on an image with alpha, the alpha channel is lost and replaced by a black background.
QImage image;
image.load("image.png");
for (int y = 0; y < image.height(); y++) {
for (int x = 0; x < image.height(); x++) {
QColor c = QColor::fromRgba(image.pixel(x, y));
c.setHsv(c.hue(), c.saturation(), c.value());
image.setPixel(x, y, c.rgba());
}
}
Here is the result when I comment out the line image.setPixel(...):
And here is the result with the image.setPixel(...) line:
I would expect my code to do no change on the image. Any idea why it's doing this?
If you look at the documentation of setHsv(), you will see that alpha is set by default to 255 (or 1.0 for the float version) if you don't explicitly specify it.
Perhaps using the line c.setHsv(c.hue(), c.saturation(), c.value(), c.alpha()); will resolve your problem.

Aero: How to draw solid (opaque) colors on glass?

Using GDI+ to draw various colors:
brush = new SolidBrush(color);
graphics.FillRectangle(brush, x, y, width, height);
You'll notice that no opaque color shows properly on glass:
How do i draw solid colors on glass?
You'll also notice that a fully opaque color is handled differently depending on what color it is:
opaque black: fully transparent
opaque color: partially transparent
opaque white: fully opaque
Can anyone point me to the documentation on the desktop compositor that explains how different colors are handled?
Update 3
You'll also notice that FillRectangle behaves differently than FillEllipse:
FillEllipse with an opaque color draws an opaque color
FillRectangle with an opaque color draws partially (or fully) transparent
Explanation for non-sensical behavior please.
Update 4
Alwayslearning suggested i change the compositing mode. From MSDN:
CompositingMode Enumeration
The CompositingMode enumeration specifies how rendered colors are combined with background colors. This enumeration is used by the Graphics::GetCompositingMode and 'Graphics::SetCompositingMode' methods of the Graphics class.
CompositingModeSourceOver
Specifies that when a color is rendered, it is blended with the background color. The blend is determined by the alpha component of the color being rendered.
CompositingModeSourceCopy
Specifies that when a color is rendered, it overwrites the background color. This mode cannot be used along with TextRenderingHintClearTypeGridFit.
From the description of CompositingModeSourceCopy, it sounds like it's not the option i want. From the limitations it imposes, it sounds like the option i want. And with composition, or transparency disabled it isn't the option i want, since it performs a SourceCopy, rather than SourceBlend:
Fortunately it's not an evil i have to contemplate because it doesn't solve my actual issue. After constructing my graphics object, i tried changed the compositing mode:
graphics = new Graphics(hDC);
graphics.SetCompositingMode(CompositingModeSourceCopy); //CompositingModeSourceCopy = 1
The result has no effect on the output:
Notes
Win32 native
not .NET (i.e. native)
not Winforms (i.e. native)
GDI+ (i.e. native)
See also
Aero: How to draw ClearType text on glass?
Windows Aero: What color to paint to make “glass” appear?
Vista/7: How to get glass color?
Seems to work OK for me. With the lack of a full code example I'm assuming you've got your compositing mode wrong.
public void RenderGdiPlus()
{
List<string> colors = new List<string>(new string[] { "000000", "ff0000", "00ff00", "0000ff", "ffffff" });
List<string> alphas = new List<string>(new string[] { "00", "01", "40", "80", "c0", "fe", "ff" });
Bitmap bmp = new Bitmap(200, 300, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
Graphics graphics = Graphics.FromImage(bmp);
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.None;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
graphics.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
SolidBrush backBrush = new SolidBrush(Color.FromArgb(254, 131, 208, 129));
graphics.FillRectangle(backBrush, 0, 0, 300, 300);
graphics.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceOver;
Pen pen = new Pen(Color.Gray);
for (int row = 0; row < alphas.Count; row++)
{
string alpha = alphas[row];
for (int column=0; column<colors.Count; column++)
{
string color = "#" + alpha + colors[column];
SolidBrush brush = new SolidBrush(ColorTranslator.FromHtml(color));
graphics.DrawRectangle(pen, 40*column, 40*row, 32, 32);
graphics.FillRectangle(brush, 1+40*column, 1+40*row, 31, 31);
}
}
Graphics gr2 = Graphics.FromHwnd(this.Handle);
gr2.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
gr2.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
gr2.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.None;
gr2.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
gr2.DrawImage(bmp, 0, 0);
}
I had a similar issue, but it involved drawing onto a layered window, rather than on Aero's glass. I haven't got any code with which I can test whether this solves your problem, but I figured it's worth a shot, since the symptoms of your problem are the same as mine.
As you have noticed, there seems to be some qwerks with FillRectangle, apparent by the differences between its behaviour and FillEllipse's.
Here are two work-arounds that I came up with, which each solve my issue:
Call FillRectangle twice
SolidBrush b(Color(254, 255, 0, 0));
gfx.FillRectangle(&b, Rect(0, 0, width, height));
gfx.FillRectangle(&b, Rect(0, 0, width, height));
Since the same area is being filled twice, they should blend and create RGB(255, 0, 0) regardless of the glass colour, which leads to a result of a 100% opaque shape. I do not prefer this method, as it requires every rectangle to be drawn twice.
Use FillPolygon instead
Just as with FillEllipse, FillPolygon doesn't seem to have the colour/opacity issue, unless you call it like so:
SolidBrush b(Color(255, 255, 0, 0));
Point points[4];
points[0] = Point(0, 0);
points[1] = Point(width, 0);
points[2] = Point(width, height);
points[4] = Point(0, height);
gfx.FillPolygon(&b, points, 4); //don't copy and paste - this won't work
For me, the above code resulted in a 100% transparent shape. I am guessing that this is either due to some form of optimisation that passes the call to FillRectangle instead. Or - most likely - there is some problem with FillPolygon, which is called by FillRectangle. Regardless, if you add an extra Point to the array, you can get around it:
SolidBrush b(Color(255, 255, 0, 0));
Point points[5];
points[0] = Point(0, 0);
points[1] = Point(0, 0); //<-
points[2] = Point(width, 0);
points[3] = Point(width, height);
points[4] = Point(0, height);
gfx.FillPolygon(&b, points, 5);
The above code indeed draws a 100% opaque shape for me. I hope this also resolves your issue.
Another day, another solution by me.
Draw everything you want to appear on glass into a bitmap.
Then, clear the form background with black color.
Immediately after this, draw the bitmap on your form.
However (as with any other solution not using DrawThemeTextEx):
Text rendering will not work correctly, because it always takes the back color of your form as an antialias/cleartype hint. Use DrawThemeTextEx instead, which also supports text with a glow effect behind.
I met the same issue with GDI.
GDI uses zero alpha channel value, so the simpliest solution is to fix alpha channel like this code does:
void fix_alpha_channel()
{
std::vector<COLORREF> pixels(cx * cy);
BITMAPINFOHEADER bmpInfo = {0};
bmpInfo.biSize = sizeof(bmpInfo);
bmpInfo.biWidth = cx;
bmpInfo.biHeight = -int(cy);
bmpInfo.biPlanes = 1;
bmpInfo.biBitCount = 32;
bmpInfo.biCompression = BI_RGB;
GetDIBits(memDc, hBmp, 0, cy, &pixels[0], (LPBITMAPINFO)&bmpInfo, DIB_RGB_COLORS);
std::for_each(pixels.begin(), pixels.end(), [](COLORREF& pixel){
if(pixel != 0) // black pixels stay transparent
pixel |= 0xFF000000; // set alpha channel to 100%
});
SetDIBits(memDc, hBmp, 0, cy, &pixels[0], (LPBITMAPINFO)&bmpInfo, DIB_RGB_COLORS);
}
I've found another way around it. Use LinearGradientBrush with both colors the same:
LinearGradientBrush brush(Point(0,0), Point(0,0), Color(255,231,45,56), Color(255,231,45,56));
g.FillRectangle(&brush, 25, 25, 30, 30);
This is perhaps slower than SolidBrush, but works fine.
Do you want a stupid solution? Here you get a stupid solution. At least it's just one line of code. And causing a small but ignorable side effect.
Assumption
When drawing solid, right angle rectangles, GDI+ tends to speed things up by drawing them in a faster method than drawing other stuff. This technique is called bitbliting. That is actually pretty clever since it is the fastest way to draw rectangles on a surface. However, the rectangles to be drawn must fulfill the rule that they are right angled.
This clever optimization was done before there was DWM, Aero, Glass and all the new fancy stuff.
Internally, bitblitting just copies the RGBA color data of pixels from one memory area to another (so to say from your drawing on your window). Sadly enough, the RGB format it writes is incompatible with glass areas, resulting in the weird transparency effects you observed.
Solution
So here comes a twist.
GDI+ can respect a transformation matrix, with which every drawing can be scaled, skewed, rotated or whatever. If we apply such a matrix, the rule that rectangles are right angled anymore is not guaranteed anymore. So, GDI+ will stop bitblitting these and draw them in a fashion similar to the ellipses.
But we also don't want to skew, scale or rotate our drawing. We simply apply the smallest transformation possible: We create a transformation matrix which moves every drawing down one pixel:
// If you don't get that matrix instance, ignore it, it's just boring math
e.Graphics.Transform = new Matrix(1f, 0.001f, 0f, 1f, 0f, 0f);
Now, bitblitting is off, rectangles are solid, violets are blue. If there would be just an easier way to control that, especially one not moving the drawings!
Thus said, if you want to draw on the first pixel row, use -1 as a Y coordinate.
You can decide if this really is a solution for you, or just ignore it.

Graphics.MeasureCharacterRanges giving wrong size calculations

I'm trying to render some text into a specific part of an image in a Web Forms app. The text will be user entered, so I want to vary the font size to make sure it fits within the bounding box.
I have code that was doing this fine on my proof-of-concept implementation, but I'm now trying it against the assets from the designer, which are larger, and I'm getting some odd results.
I'm running the size calculation as follows:
StringFormat fmt = new StringFormat();
fmt.Alignment = StringAlignment.Center;
fmt.LineAlignment = StringAlignment.Near;
fmt.FormatFlags = StringFormatFlags.NoClip;
fmt.Trimming = StringTrimming.None;
int size = __startingSize;
Font font = __fonts.GetFontBySize(size);
while (GetStringBounds(text, font, fmt).IsLargerThan(__textBoundingBox))
{
context.Trace.Write("MyHandler.ProcessRequest",
"Decrementing font size to " + size + ", as size is "
+ GetStringBounds(text, font, fmt).Size()
+ " and limit is " + __textBoundingBox.Size());
size--;
if (size < __minimumSize)
{
break;
}
font = __fonts.GetFontBySize(size);
}
context.Trace.Write("MyHandler.ProcessRequest", "Writing " + text + " in "
+ font.FontFamily.Name + " at " + font.SizeInPoints + "pt, size is "
+ GetStringBounds(text, font, fmt).Size()
+ " and limit is " + __textBoundingBox.Size());
I then use the following line to render the text onto an image I'm pulling from the filesystem:
g.DrawString(text, font, __brush, __textBoundingBox, fmt);
where:
__fonts is a PrivateFontCollection,
PrivateFontCollection.GetFontBySize is an extension method that returns a FontFamily
RectangleF __textBoundingBox = new RectangleF(150, 110, 212, 64);
int __minimumSize = 8;
int __startingSize = 48;
Brush __brush = Brushes.White;
int size starts out at 48 and decrements within that loop
Graphics g has SmoothingMode.AntiAlias and TextRenderingHint.AntiAlias set
context is a System.Web.HttpContext (this is an excerpt from the ProcessRequest method of an IHttpHandler)
The other methods are:
private static RectangleF GetStringBounds(string text, Font font,
StringFormat fmt)
{
CharacterRange[] range = { new CharacterRange(0, text.Length) };
StringFormat myFormat = fmt.Clone() as StringFormat;
myFormat.SetMeasurableCharacterRanges(range);
using (Graphics g = Graphics.FromImage(new Bitmap(
(int) __textBoundingBox.Width - 1,
(int) __textBoundingBox.Height - 1)))
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
Region[] regions = g.MeasureCharacterRanges(text, font,
__textBoundingBox, myFormat);
return regions[0].GetBounds(g);
}
}
public static string Size(this RectangleF rect)
{
return rect.Width + "×" + rect.Height;
}
public static bool IsLargerThan(this RectangleF a, RectangleF b)
{
return (a.Width > b.Width) || (a.Height > b.Height);
}
Now I have two problems.
Firstly, the text sometimes insists on wrapping by inserting a line-break within a word, when it should just fail to fit and cause the while loop to decrement again. I can't see why it is that Graphics.MeasureCharacterRanges thinks that this fits within the box when it shouldn't be word-wrapping within a word. This behaviour is exhibited irrespective of the character set used (I get it in Latin alphabet words, as well as other parts of the Unicode range, like Cyrillic, Greek, Georgian and Armenian). Is there some setting I should be using to force Graphics.MeasureCharacterRanges only to be word-wrapping at whitespace characters (or hyphens)? This first problem is the same as post 2499067.
Secondly, in scaling up to the new image and font size, Graphics.MeasureCharacterRanges is giving me heights that are wildly off. The RectangleF I am drawing within corresponds to a visually apparent area of the image, so I can easily see when the text is being decremented more than is necessary. Yet when I pass it some text, the GetBounds call is giving me a height that is almost double what it's actually taking.
Using trial and error to set the __minimumSize to force an exit from the while loop, I can see that 24pt text fits within the bounding box, yet Graphics.MeasureCharacterRanges is reporting that the height of that text, once rendered to the image, is 122px (when the bounding box is 64px tall and it fits within that box). Indeed, without forcing the matter, the while loop iterates to 18pt, at which point Graphics.MeasureCharacterRanges returns a value that fits.
The trace log excerpt is as follows:
Decrementing font size to 24, as size is 193×122 and limit is 212×64
Decrementing font size to 23, as size is 191×117 and limit is 212×64
Decrementing font size to 22, as size is 200×75 and limit is 212×64
Decrementing font size to 21, as size is 192×71 and limit is 212×64
Decrementing font size to 20, as size is 198×68 and limit is 212×64
Decrementing font size to 19, as size is 185×65 and limit is 212×64
Writing VENNEGOOR of HESSELINK in DIN-Black at 18pt, size is 178×61 and limit is 212×64
So why is Graphics.MeasureCharacterRanges giving me a wrong result? I could understand it being, say, the line height of the font if the loop stopped around 21pt (which would visually fit, if I screenshot the results and measure it in Paint.Net), but it's going far further than it should be doing because, frankly, it's returning the wrong damn results.
I have a similar problem. I want to know how big the text I'm drawing is going to be, and where it's going to appear, EXACTLY. I haven't had the line-break problem, so I don't think I can help you there. I had the same problems you had with all the various measuring techniques available, including ending up with MeasureCharacterRanges, which worked okay for the left and right, but not at all for the height and top. (Playing with the baseline can work well for some rare applications though.)
I've ended up with a very inelegant, inefficient, but working solution, at least for my use case. I draw the text on a bitmap, check the bits to see where they ended up, and that's my range. Since I'm mostly drawing small fonts and short strings, it's been fast enough for me (especially with the memoization I added). Maybe this won't be exactly what you need, but maybe it can lead you down the right track anyway.
Note it requires compiling the project to allow unsafe code at the moment, as I'm trying to squeeze out every bit of efficiency from it, but that constraint could be removed if you wanted to. Also, it's not as thread safe as it could be right now, you could easily add that if you needed it.
Dictionary<Tuple<string, Font, Brush>, Rectangle> cachedTextBounds = new Dictionary<Tuple<string, Font, Brush>, Rectangle>();
/// <summary>
/// Determines bounds of some text by actually drawing the text to a bitmap and
/// reading the bits to see where it ended up. Bounds assume you draw at 0, 0. If
/// drawing elsewhere, you can easily offset the resulting rectangle appropriately.
/// </summary>
/// <param name="text">The text to be drawn</param>
/// <param name="font">The font to use when drawing the text</param>
/// <param name="brush">The brush to be used when drawing the text</param>
/// <returns>The bounding rectangle of the rendered text</returns>
private unsafe Rectangle RenderedTextBounds(string text, Font font, Brush brush) {
// First check memoization
Tuple<string, Font, Brush> t = new Tuple<string, Font, Brush>(text, font, brush);
try {
return cachedTextBounds[t];
}
catch(KeyNotFoundException) {
// not cached
}
// Draw the string on a bitmap
Rectangle bounds = new Rectangle();
Size approxSize = TextRenderer.MeasureText(text, font);
using(Bitmap bitmap = new Bitmap((int)(approxSize.Width*1.5), (int)(approxSize.Height*1.5))) {
using(Graphics g = Graphics.FromImage(bitmap))
g.DrawString(text, font, brush, 0, 0);
// Unsafe LockBits code takes a bit over 10% of time compared to safe GetPixel code
BitmapData bd = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
byte* row = (byte*)bd.Scan0;
// Find left, looking for first bit that has a non-zero alpha channel, so it's not clear
for(int x = 0; x < bitmap.Width; x++)
for(int y = 0; y < bitmap.Height; y++)
if(((byte*)bd.Scan0)[y*bd.Stride + 4*x + 3] != 0) {
bounds.X = x;
goto foundX;
}
foundX:
// Right
for(int x = bitmap.Width - 1; x >= 0; x--)
for(int y = 0; y < bitmap.Height; y++)
if(((byte*)bd.Scan0)[y*bd.Stride + 4*x + 3] != 0) {
bounds.Width = x - bounds.X + 1;
goto foundWidth;
}
foundWidth:
// Top
for(int y = 0; y < bitmap.Height; y++)
for(int x = 0; x < bitmap.Width; x++)
if(((byte*)bd.Scan0)[y*bd.Stride + 4*x + 3] != 0) {
bounds.Y = y;
goto foundY;
}
foundY:
// Bottom
for(int y = bitmap.Height - 1; y >= 0; y--)
for(int x = 0; x < bitmap.Width; x++)
if(((byte*)bd.Scan0)[y*bd.Stride + 4*x + 3] != 0) {
bounds.Height = y - bounds.Y + 1;
goto foundHeight;
}
foundHeight:
bitmap.UnlockBits(bd);
}
cachedTextBounds[t] = bounds;
return bounds;
}
Ok so 4 years late but this question EXACTLY matched my symptoms and I've actually worked out the cause.
There is most certainly a bug in MeasureString AND MeasureCharacterRanges.
The simple answer is:
Make sure you divide your width restriction (int width in MeasureString or the Size.Width property of the boundingRect in MeasureCharacterRanges) by 0.72. When you get your results back multiply each dimension by 0.72 to get the REAL result
int measureWidth = Convert.ToInt32((float)width/0.72);
SizeF measureSize = gfx.MeasureString(text, font, measureWidth, format);
float actualHeight = measureSize.Height * (float)0.72;
or
float measureWidth = width/0.72;
Region[] regions = gfx.MeasureCharacterRanges(text, font, new RectangleF(0,0,measureWidth, format);
float actualHeight = 0;
if(regions.Length>0)
{
actualHeight = regions[0].GetBounds(gfx).Size.Height * (float)0.72;
}
The explanation (that I can figure out) is that something to do with the context is triggering a conversion in the Measure methods (that doesn't trigger in the DrawString method) for inch->point (*72/100). When you pass in the ACTUAL width limitation it is adjusting this value so the MEASURED width limitation is, in effect, shorter than it should be. Your text then wraps earlier than it is supposed to and so you get a longer height result than expected. Unfortunately the conversion applies to the actual height result as well so it's a good idea to 'unconvert' that value too.
Could you try removing the following line?
fmt.FormatFlags = StringFormatFlags.NoClip;
Overhanging parts of glyphs, and
unwrapped text reaching outside the
formatting rectangle are allowed to
show. By default all text and glyph
parts reaching outside the formatting
rectangle are clipped.
That's the best I can come up with for this :(
I also had some problems with the MeasureCharacterRanges method. It was giving me inconsistent sizes for the same string and even the same Graphics object. Then I discovered that it depends on the value of the layoutRect parametr - I can't see why, in my opinion it's a bug in the .NET code.
For example if layoutRect was completely empty (all values set to zero), I got correct values for the string "a" - the size was {Width=8.898438, Height=18.10938} using 12pt Ms Sans Serif font.
However, when I set the value of the 'X' property of the rectangle to a non-integer number (like 1.2), it gave me {Width=9, Height=19}.
So I really think there is a bug when you use a layout rectangle with non-integer X coordinate.
To convert from points to dpi as in screen resolution you need to divide by 72 and multiply by DPI, for example:
graphics.DpiY * text.Width / 72
Red Nightengale was really close, because graphics.DpiY is usually 96 for screen resolutions.

Resources