Graphics.MeasureCharacterRanges дает неправильные вычисления размера

Я пытаюсь отобразить текст в определенной части изображения в приложении веб-форм. Текст будет вводиться пользователем, поэтому я хочу изменить размер шрифта, чтобы убедиться, что он помещается в ограничивающую рамку.

У меня есть код, который отлично справлялся с моей проверочной концепцией, но теперь я пробую его с активами от дизайнера, которые больше, и получаю некоторые странные результаты.

Я выполняю расчет размера следующим образом:

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 is a 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, т.

Так почему же Graphics.MeasureCharacterRanges дает неверный результат? Я мог бы понять, что это, скажем, высота строки шрифта, если бы цикл остановился на отметке 21pt (что визуально подошло бы, если бы я сделал снимок экрана с результатами и измерил его в Paint.Net), но он идет намного дальше, чем должен. потому что, честно говоря, он возвращает чертовски неверные результаты.


person Owen Blacker    schedule 21.04.2010    source источник
comment
+1 за один из самых задокументированных вопросов, которые я видел за последнее время!   -  person SouthShoreAK    schedule 18.04.2012


Ответы (5)


У меня похожая проблема. Я хочу знать, насколько большим будет текст, который я рисую, и где он будет ИМЕННО. У меня не было проблемы с разрывом строки, поэтому я не думаю, что смогу вам здесь помочь. У меня были те же проблемы, что и у вас со всеми доступными методами измерения, в том числе с использованием 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;
}
person user12861    schedule 15.08.2011
comment
Хорошо сделано! Я должен попробовать и посмотреть, решит ли это мою проблему. Хотя мне несколько неприятно полагаться на unsafe код. Как быстро вы находите этот код? Обстоятельства, которые я использую, проверяют, не слишком ли большой текст для ограничивающего прямоугольника фиксированного размера, и уменьшает размер шрифта в пределах цикла while (проверка while too big or size gt hard-floor-limit), поэтому я не хочу выполнять дорогостоящую операцию в этом while петля… - person Owen Blacker; 16.08.2011
comment
Скорость сильно зависит от размера шрифта. Он выделяет растровое изображение, отображает его, а затем ищет границы. Меньшие шрифты работают намного быстрее, и я думаю, что с большими шрифтами вы теряете что-то вроде O (размер ^ 2). Возможно, вам захочется угадать маленький размер и поработать до слишком большого, а не наоборот. Вы можете использовать MeasureCharacterRanges в качестве первого предположения для нижней границы, поскольку оно всегда оказывается слишком маленьким предположением. Есть много возможностей для умного поиска нужного размера, бинарного поиска с умным угадыванием и т. Д. Мне это не нужно для моей цели, поэтому я не играл с этим. - person user12861; 16.08.2011
comment
Что касается небезопасности, то вы можете обойтись и без этого, но скорость сильно страдает. Как уже говорилось, это почти на 90% быстрее, чем использование GetPixel, но вы все равно можете использовать промежуточный метод с помощью LockBits без небезопасного кода. Я не пробовал этого, потому что меня устраивал небезопасный код, и я хотел выжать немного легко получаемой производительности. Есть много способов попытаться выжать производительность в зависимости от того, как именно вы это используете, но все они сложны. Я полностью понимаю, что все это довольно неприятно. Я бы подумал, что есть способ получше, но мой поиск в Интернете был таким же непродуктивным, как и ваш. - person user12861; 16.08.2011
comment
Это действительно полезно, user12861; Спасибо. Надеюсь, у меня скоро появится шанс попробовать это. - person Owen Blacker; 25.08.2011

Итак, опоздал на 4 года, но этот вопрос ТОЧНО соответствовал моим симптомам, и я действительно выяснил причину.

Совершенно определенно есть ошибка в MeasureString И 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;

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;
}

Объяснение (которое я могу понять) заключается в том, что что-то, связанное с контекстом, вызывает преобразование в методах Measure (которое не запускается в методе DrawString) для inch-> point (* 72/100). Когда вы передаете ограничение ФАКТИЧЕСКОЙ ширины, оно регулирует это значение таким образом, чтобы ограничение ИЗМЕРЕННОЙ ширины было, по сути, короче, чем должно быть. Затем ваш текст переносится раньше, чем предполагалось, и поэтому вы получаете более длинный результат, чем ожидалось. К сожалению, преобразование применимо и к фактическому результату высоты, поэтому неплохо было бы «преобразовать» и это значение.

person Red Nightingale    schedule 16.11.2014
comment
На ДЕЙСТВИТЕЛЬНО раздражающей стороне, я абсолютно не знаю, почему MeasureString принимает int для ширины. Если вы установите для своей графической контекстной единицы измерения дюймы, тогда (из-за int) вы можете установить ограничение ширины только на 1 дюйм или 2 дюйма и т. Д. Совершенно нелепо! - person Red Nightingale; 16.11.2014

Не могли бы вы попробовать удалить следующую строку?

fmt.FormatFlags = StringFormatFlags.NoClip;

Допускаются выступающие части глифов и развернутый текст, выходящий за пределы прямоугольника форматирования. По умолчанию все части текста и глифов, выходящие за пределы прямоугольника форматирования, обрезаются.

Это лучшее, что я могу придумать для этого :(

person Codesleuth    schedule 29.04.2010
comment
Спасибо за ответ. Я действительно смотрел на это, подумав, что это может быть проблемой, но заявленная высота почти вдвое превышает фактическую высоту, поэтому я не думаю, что это может быть так. Кажется, что разница, которую делает StringFormatFlags.NoClip, заключается в том, что если чаша с буквой P (например) просто выходит за пределы ограничивающей рамки, тогда ее разрешается визуализировать, а не обрезать. Кажется, я не испытываю этой проблемы. Но спасибо: о) - person Owen Blacker; 29.04.2010

У меня тоже были проблемы с методом MeasureCharacterRanges. Это давало мне несовместимые размеры для одной и той же строки и даже для одного и того же Graphics объекта. Затем я обнаружил, что это зависит от значения параметра layoutRect - я не понимаю, почему, на мой взгляд, это ошибка в коде .NET.

Например, если layoutRect был полностью пустым (все значения установлены на ноль), я получил правильные значения для строки «a» - размер был {Width=8.898438, Height=18.10938} с использованием шрифта Ms Sans Serif размером 12 пунктов.

Однако, когда я установил значение свойства «X» прямоугольника на нецелое число (например, 1.2), это дало мне {Width=9, Height=19}.

Поэтому я действительно думаю, что есть ошибка, когда вы используете прямоугольник макета с нецелой координатой X.

person Pavel    schedule 09.08.2010
comment
Интересно - похоже, он всегда округляет ваш размер в большую сторону. К сожалению, мой layoutRect всегда целочислен - он определяется как частный статический только для чтения RectangleF __textBoundingBox = new RectangleF (150, 110, 212, 64); и его значение никогда не меняется (очевидно, поскольку оно помечено только для чтения). Это определенно ошибка в коде .Net, но не похоже, что ваша ошибка и моя ошибка совпадают. - person Owen Blacker; 09.08.2010

Чтобы преобразовать точки в dpi, как в разрешении экрана, вам нужно разделить на 72 и умножить на DPI, например: graphics.DpiY * text.Width / 72

Red Nightengale был действительно близок, потому что graphics.DpiY обычно 96 для разрешений экрана.

person JBStCyr    schedule 10.11.2018
comment
Однако это не ответ на вопрос в целом. Не могли бы вы расширить это до ответа, включая любой ответ Красного Соловья, который вам нужно продублировать? Или опубликуйте это как комментарий к ответу Red Nightingale? Спасибо! - person Owen Blacker; 12.11.2018