Graphics.MeasureCharacterRanges дает неверные расчеты размера
Я пытаюсь сделать текст в определенной части изображения в приложении Web Forms. Текст будет введен пользователем, поэтому я хочу изменить размер шрифта, чтобы убедиться, что он вписывается в ограничивающую рамку.
У меня есть код, который отлично справлялся с моей проверкой концепции, но сейчас я пробую его на ресурсах дизайнера, которые больше, и я получаю некоторые странные результаты.
Я выполняю расчет размера следующим образом:
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());
Затем я использую следующую строку для рендеринга текста на изображение, которое я извлекаю из файловой системы:
g.DrawString(text, font, __brush, __textBoundingBox, fmt);
где:
__fonts
этоPrivateFontCollection
,PrivateFontCollection.GetFontBySize
это метод расширения, который возвращаетFontFamily
RectangleF __textBoundingBox = new RectangleF(150, 110, 212, 64);
int __minimumSize = 8;
int __startingSize = 48;
Brush __brush = Brushes.White;
int size
начинается в 48 и уменьшается в этом циклеGraphics g
имеетSmoothingMode.AntiAlias
а такжеTextRenderingHint.AntiAlias
задаватьcontext
этоSystem.Web.HttpContext
(это выдержка изProcessRequest
методIHttpHandler
)
Другие методы:
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);
}
Теперь у меня две проблемы.
Во-первых, текст иногда настаивает на переносе, вставляя в слово разрыв строки, когда он просто не помещается и снова вызывает цикл while. Я не могу понять, почему это так Graphics.MeasureCharacterRanges
считает, что это вписывается в рамки, когда не должно быть перенос слов в слове. Это поведение демонстрируется независимо от используемого набора символов (я получаю его в словах латинского алфавита, а также в других частях диапазона Unicode, таких как кириллица, греческий, грузинский и армянский). Есть ли какие-то настройки, которые я должен использовать, чтобы заставить Graphics.MeasureCharacterRanges
только для переноса слов в пробелах (или дефисах)? Эта первая проблема такая же, как пост 2499067.
Во-вторых, при масштабировании до нового изображения и размера шрифта, Graphics.MeasureCharacterRanges
дает мне дикие высоты RectangleF
Я рисую внутри соответствует визуально видимой области изображения, поэтому я могу легко видеть, когда текст уменьшается больше, чем необходимо. Тем не менее, когда я передаю текст, GetBounds
колл дает мне высоту, которая почти вдвое больше, чем на самом деле.
Используя метод проб и ошибок, чтобы установить __minimumSize
чтобы заставить выйти из цикла while, я вижу, что 24pt текст помещается в ограничивающий прямоугольник, но Graphics.MeasureCharacterRanges
сообщает, что высота этого текста, когда-то отображаемого на изображении, составляет 122 пикселя (когда ограничивающий прямоугольник имеет высоту 64 пикселя, и он вписывается в этот прямоугольник). Действительно, без форсирования, цикл while повторяется до 18pt, после чего Graphics.MeasureCharacterRanges
возвращает значение, которое подходит.
Выдержка из журнала трассировки выглядит следующим образом:
Уменьшение размера шрифта до 24, поскольку размер равен 193×122, а ограничение равно 212×64.
Уменьшение размера шрифта до 23, так как размер равен 191×117, а ограничение равно 212×64.
Уменьшение размера шрифта до 22, так как размер равен 200×75, а ограничение равно 212×64.
Уменьшение размера шрифта до 21, так как размер равен 192×71, а ограничение равно 212×64.
Уменьшение размера шрифта до 20, поскольку размер равен 198×68, а ограничение равно 212×64.
Уменьшение размера шрифта до 19, так как размер равен 185×65, а ограничение равно 212×64.
Написание VENNEGOOR из HESSELINK на DIN-черном в 18pt, размер 178×61 и ограничение 212×64
Так почему же Graphics.MeasureCharacterRanges
дает мне неправильный результат? Я мог бы понять, что это, скажем, высота строки шрифта, если цикл остановился около 21 пт (что визуально соответствовало бы, если бы я скриншотил результаты и измерил его в Paint.Net), но он идет гораздо дальше, чем следовало бы делать потому что, честно говоря, это возвращает неправильные чертовы результаты.
5 ответов
Хорошо, так что 4 года поздно, но этот вопрос точно соответствует моим симптомам, и я на самом деле выяснил причину.
Скорее всего, есть ошибка в MeasureString AND MeasureCharacterRanges.
Ответ прост: убедитесь, что вы делите ограничение ширины (int width в MeasureString или свойство Size.Width boundingRect в MeasureCharacterRanges) на 0,72. Когда вы получите свои результаты обратно, умножьте каждое измерение на 0,72, чтобы получить РЕАЛЬНЫЙ результат
int measureWidth = Convert.ToInt32((float)width/0.72);
SizeF measureSize = gfx.MeasureString(text, font, measureWidth, format);
float actualHeight = measureSize.Height * (float)0.72;
или же
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;
}
Объяснение (которое я могу выяснить) состоит в том, что что-то, что связано с контекстом, вызывает преобразование в методах Measure (которое не вызывается в методе DrawString) для дюйма-> точка (*72/100). Когда вы передаете фактическое ограничение ширины, оно корректирует это значение, так что измеренное ограничение ширины, по сути, короче, чем должно быть. Затем ваш текст переносится раньше, чем предполагалось, и поэтому вы получите более высокий результат по высоте, чем ожидалось. К сожалению, преобразование применимо и к фактическому результату высоты, так что неплохо бы также "преобразовать" это значение.
У меня похожая проблема. Я хочу знать, насколько большим будет текст, который я рисую, и где он появится, ТОЧНО. У меня не было проблемы с разрывом строки, поэтому я не думаю, что смогу вам там помочь. У меня были те же проблемы, что и у вас, со всеми доступными методами измерения, в том числе с MeasureCharacterRanges, который работал нормально для левой и правой стороны, но не для высоты и высоты. (Игра с базовой линией может работать хорошо для некоторых редких приложений.)
Я получил очень неэффективное, неэффективное, но работающее решение, по крайней мере, для моего случая использования. Я рисую текст на растровом изображении, проверяю биты, чтобы увидеть, где они оказались, и это мой диапазон. Поскольку я в основном рисую мелкие шрифты и короткие строки, это было достаточно быстро для меня (особенно с добавленной мной памяткой). Может быть, это будет не совсем то, что вам нужно, но, может быть, это в любом случае приведет вас на правильный путь.
Обратите внимание, что сейчас требуется компиляция проекта, чтобы разрешить небезопасный код, так как я пытаюсь выжать из него каждый бит эффективности, но это ограничение можно снять, если вы захотите. Кроме того, это не так безопасно для потоков, как могло бы быть сейчас, вы можете легко добавить это, если вам это нужно.
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;
}
Не могли бы вы попробовать удалить следующую строку?
fmt.FormatFlags = StringFormatFlags.NoClip;
Разрешается показывать выступающие части глифов и развернутый текст, выходящий за пределы прямоугольника форматирования. По умолчанию все части текста и глифа, выходящие за пределы прямоугольника форматирования, обрезаются.
Это лучшее, что я могу придумать для этого:(
Чтобы преобразовать точки в точки на дюйм, как в разрешении экрана, необходимо разделить их на 72 и умножить на DPI, например: graphics.DpiY * text.Width / 72
Красный Nightengale был действительно близко, потому что graphics.DpiY обычно составляет 96 для разрешения экрана.
У меня также были некоторые проблемы с MeasureCharacterRanges
метод. Это дало мне непоследовательные размеры для одной и той же строки и даже одного и того же Graphics
объект. Тогда я обнаружил, что это зависит от стоимости layoutRect
Параметр - я не понимаю, почему, по моему мнению, это ошибка в коде.NET.
Например, если layoutRect
был полностью пуст (все значения установлены на ноль), я получил правильные значения для строки "а" - размер был {Width=8.898438, Height=18.10938}
используя шрифт 12pt Ms Sans Serif.
Однако, когда я установил значение свойства 'X' прямоугольника на нецелое число (например, 1.2), это дало мне {Width=9, Height=19}
,
Поэтому я действительно считаю, что при использовании прямоугольника макета с нецелой координатой X возникает ошибка.