Graphics.MeasureCharacterRanges дает неверные расчеты размера - PullRequest
6 голосов
/ 21 апреля 2010

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

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

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

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 установлено
  • contextSystem.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);
}

Теперь у меня две проблемы.

Во-первых, теxt иногда настаивает на переносе, вставляя в слово разрыв строки, когда он просто не помещается и заставляет цикл while снова уменьшаться.Я не могу понять, почему Graphics.MeasureCharacterRanges думает, что это вписывается в рамку, когда это не должно быть переносом слов в слове.Это поведение демонстрируется независимо от используемого набора символов (я получаю его в словах латинского алфавита, а также в других частях диапазона Unicode, таких как кириллица, греческий, грузинский и армянский).Есть ли какая-то настройка, которую я должен использовать, чтобы заставить Graphics.MeasureCharacterRanges только перенос слов в пробельные символы (или дефисы)?Эта первая проблема такая же, как post 2499067 .

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

Использование метода проб и ошибок для установки __minimumSize для принудительного выхода изВ цикле я вижу, что 24pt текст помещается в ограничивающий прямоугольник, но Graphics.MeasureCharacterRanges сообщает, что высота этого текста, после того, как он был представлен на изображение, составляет 122px (когда ограничивающий прямоугольник имеет высоту 64px, и он помещается внутри этого прямоугольника),Действительно, без форсирования вопроса цикл 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 ]

1 голос
/ 16 ноября 2014

Хорошо, так на 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) , Когда вы передаете фактическое ограничение ширины, оно корректирует это значение, поэтому измеренное ограничение ширины, по сути, короче, чем должно быть. Затем ваш текст переносится раньше, чем предполагалось, и поэтому вы получите более высокий результат по высоте, чем ожидалось. К сожалению, преобразование применимо и к фактическому результату высоты, поэтому неплохо также преобразовать это значение.

1 голос
/ 15 августа 2011

У меня похожая проблема. Я хочу знать, насколько большим будет текст, который я рисую, и где он появится, ТОЧНО. У меня не было проблемы с разрывом строки, поэтому я не думаю, что смогу вам там помочь. У меня были те же проблемы, что и у вас, со всеми доступными методами измерения, в том числе с 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;
}
0 голосов
/ 11 ноября 2018

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

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

0 голосов
/ 09 августа 2010

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

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

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

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

0 голосов
/ 29 апреля 2010

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

fmt.FormatFlags = StringFormatFlags.NoClip;

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

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

...