2010-04-21 16 views
5

Estoy tratando de representar texto en una parte específica de una imagen en una aplicación Web Forms. El usuario ingresará el texto, por lo que quiero variar el tamaño de la fuente para garantizar que encaje dentro del cuadro delimitador.Graphics.MeasureCharacterRanges dando cálculos de tamaño incorrecto

Tengo un código que estaba haciendo esto bien en mi implementación de prueba de concepto, pero ahora lo estoy intentando contra los activos del diseñador, que son más grandes, y estoy obteniendo algunos resultados extraños.

estoy ejecutando el cálculo del tamaño de la siguiente manera:

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()); 

Luego utilizo la siguiente línea al representar el texto en una imagen que estoy tirando desde el sistema de archivos:

g.DrawString(text, font, __brush, __textBoundingBox, fmt); 

donde :

  • __fonts es una PrivateFontCollection,
  • PrivateFontCollection.GetFontBySize es un método de extensión que devuelve un FontFamily
  • RectangleF __textBoundingBox = new RectangleF(150, 110, 212, 64);
  • int __minimumSize = 8;
  • int __startingSize = 48;
  • Brush __brush = Brushes.White;
  • int size comienza en 48 y decrementos dentro de ese bucle
  • Graphics g tiene SmoothingMode.AntiAlias y TextRenderingHint.AntiAlias conjunto
  • context es una System.Web.HttpContext (este es un extracto del método de un IHttpHandlerProcessRequest)

Los otros métodos son:

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

Ahora tienen dos problemas.

En primer lugar, el texto a veces insiste en envolver insertando un salto de línea dentro de una palabra, cuando simplemente no se ajusta y hace que el ciclo while se reduzca nuevamente. No puedo ver por qué Graphics.MeasureCharacterRanges piensa que esto encaja dentro del cuadro cuando no debería ser un término en una palabra. Este comportamiento se exhibe independientemente del conjunto de caracteres utilizado (lo obtengo en palabras del alfabeto latino, así como en otras partes de la gama Unicode, como cirílico, griego, georgiano y armenio). ¿Hay alguna configuración que deba usar para forzar Graphics.MeasureCharacterRanges solo para que se ajuste la palabra en los espacios en blanco de los caracteres (o guiones)? Este primer problema es el mismo que post 2499067.

En segundo lugar, en la ampliación de la nueva imagen y el tamaño de fuente, Graphics.MeasureCharacterRanges me está dando alturas que están desenfrenadas. El RectangleF que estoy dibujando corresponde a un área visualmente visible de la imagen, por lo que puedo ver fácilmente cuando el texto se reduce más de lo necesario. Sin embargo, cuando le paso algo de texto, la llamada GetBounds me da una altura que es casi el doble de lo que realmente está tomando.

El uso de ensayo y error para ajustar la __minimumSize para forzar una salida del bucle while, puedo ver que el texto 24pt encaja dentro del cuadro delimitador, sin embargo Graphics.MeasureCharacterRanges informa que la altura de ese texto, una vez que vuelve a la imagen, es 122px (cuando el cuadro delimitador tiene 64px de alto y cabe dentro de esa caja). De hecho, sin forzar la cuestión, el ciclo while itera a 18pt, en cuyo punto Graphics.MeasureCharacterRanges devuelve un valor que se ajusta.

El extracto del registro de seguimiento es como sigue:

decrementar el tamaño de fuente a 24, como el tamaño es de 193 × 122 y el límite es 212 × 64
decrementar el tamaño de fuente a 23, como el tamaño es de 191 × 117 y el límite es 212 × 64
decrementar el tamaño de fuente a 22, como el tamaño es de 200 × 75 y el límite es 212 × 64
decrementar tamaño de la fuente a 21, como el tamaño es de 192 × 71 y el límite es 212 × fuente 64
decrementar tamaño a 20, ya que el tamaño es 198 × 68 y el límite es 212 × 64
Decre Menting tamaño de la fuente a 19, como el tamaño es de 185 × 65 y el límite es de 212 × 64
escritura Vennegoor of Hesselink en la norma DIN-Negro a 18 puntos, el tamaño es de 178 × 61 y el límite es de 212 × 64

Entonces, ¿por es Graphics.MeasureCharacterRanges que me da un resultado incorrecto? Podría entender que es, por ejemplo, la altura de la línea de la fuente si el ciclo se detuvo alrededor de 21pt (que se ajustaría visualmente, si capturé los resultados y lo mido en Paint.Net), pero va mucho más lejos de lo que debería estar haciendo porque, francamente, está devolviendo los malditos resultados equivocados.

+0

+1 para una de las preguntas más documentadas que he visto en mucho tiempo. – SouthShoreAK

Respuesta

0

¿Podría intentar eliminar la siguiente línea?

fmt.FormatFlags = StringFormatFlags.NoClip; 

partes saliente del glifos, y sin envolver texto que llegan fuera de la formatear rectángulo se les permite show. De forma predeterminada, se recortan todas las partes de texto y glifo que salen del formato .

Eso es lo mejor que se puede llegar a este :(

+0

Gracias por publicar una respuesta. Miré esto, habiendo pensado que podría ser el problema, pero la altura informada es casi el doble de la altura real, así que no creo que pueda ser eso. Parece que la diferencia que hace StringFormatFlags.NoClip es que si el cuenco de una letra P (por ejemplo) simplemente pasa fuera del cuadro delimitador, se permite procesar, en lugar de recortar. Ese no parece ser el problema que estoy experimentando. Pero gracias: o) –

0

También tuve algunos problemas con el método MeasureCharacterRanges. Se me estaba dando tamaños inconsistentes para la misma cadena, e incluso el mismo Graphics objeto. A continuación, Descubrí que depende del valor del parámetro layoutRect - No puedo ver por qué, en mi opinión es un error en el código .NET

Por ejemplo, si layoutRect estaba completamente vacío (todos los valores configurados en cero) , Obtuve los valores correctos para la cadena "a" - el tamaño era {Width=8.898438, Height=18.10938} usando 12pt Fuente de la Sra. Sans Serif.

Sin embargo, cuando establecí el valor de la propiedad 'X' del rectángulo en un número no entero (como 1.2), me dio {Width=9, Height=19}.

Así que realmente creo que hay un error cuando se usa un rectángulo de diseño con una coordenada X no entera.

+0

Interesante: parece que siempre está redondeando su tamaño. Desafortunadamente, mi layoutRect siempre es integral: está definido como privado estático de solo lectura RectangleF __textBoundingBox = new RectangleF (150, 110, 212, 64); y su valor nunca cambia (obviamente, ya que está marcado de solo lectura). Definitivamente es un error en el código .Net, pero no parece que tu error y mi error sean el mismo. –

1

Tengo un problema similar. Quiero saber qué tan grande va a ser el texto que estoy dibujando, y dónde va a aparecer, EXACTAMENTE. No he tenido el problema de salto de línea, así que no creo poder ayudarte allí.Tuve los mismos problemas que tenía con todas las diversas técnicas de medición disponibles, incluido terminar con MeasureCharacterRanges, que funcionó bien para la izquierda y la derecha, pero no para la altura y la parte superior. (Jugar con la línea base puede funcionar bien para algunas aplicaciones raras).

Terminé con una solución muy poco elegante, ineficiente, pero funcional, al menos para mi caso de uso. Dibujé el texto en un mapa de bits, revisé los bits para ver dónde terminaban, y ese es mi alcance. Dado que principalmente estoy dibujando fuentes pequeñas y cadenas cortas, ha sido lo suficientemente rápido para mí (especialmente con las memorias que agregué). Tal vez esto no sea exactamente lo que necesita, pero de todos modos puede llevarlo por el camino correcto.

Tenga en cuenta que es necesario compilar el proyecto para permitir el código inseguro en este momento, ya que estoy tratando de sacarle todo el rendimiento, pero esa restricción podría eliminarse si lo desea. Además, no es tan seguro para subprocesos como podría ser ahora, podría agregarlo fácilmente si lo necesitara.

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

¡Buen trabajo! Tendré que intentarlo y ver si eso resuelve mi problema. Aunque estoy un poco reacio a tener que confiar en el código 'inseguro'. ¿Qué tan rápido encuentras ese código?La circunstancia que estoy usando comprueba si el texto es demasiado grande para un cuadro delimitador de tamaño fijo y disminuye el tamaño de la fuente dentro de un ciclo 'while' (verificando' si es demasiado grande o talla gt hard-floor-limit'), así que no quiero realizar una operación costosa en ese bucle 'while' ... –

+0

La velocidad depende en gran medida del tamaño de la fuente. Asigna mapa de bits, lo renderiza y luego busca los límites. Las fuentes más pequeñas son mucho más rápidas, y se pierde con fuentes más grandes, como O (tamaño^2), creo. Puede querer adivinar el tamaño pequeño y trabajar hasta un tamaño demasiado grande en lugar de lo contrario. Podría usar MeasureCharacterRanges como primera aproximación para el límite inferior, ya que siempre parece ser una conjetura demasiado pequeña. Hay mucho espacio para la inteligencia buscando el tamaño correcto, búsqueda binaria con adivinanzas, etc. No necesito nada de eso para mi propósito, así que no he jugado con eso. – user12861

+0

En cuanto a la inseguridad, puedes hacerlo sin eso, pero la velocidad es un éxito. Como se comentó, esto es casi un 90% más rápido que usar GetPixel, pero aún se puede hacer un método intermedio utilizando LockBits sin código inseguro. No intenté eso ya que estaba bien con el código inseguro y quería exprimir un poco de rendimiento fácilmente obtenido. Hay muchas maneras de tratar de exprimir el rendimiento dependiendo de cómo exactamente uses estas cosas, pero son todas complicadas. Todo esto es desagradable, me di cuenta completamente. Pensaría que habría una mejor manera, pero mi búsqueda en línea fue tan improductiva como la tuya. – user12861

0

Ok 4 años tarde pero esta pregunta coincidió EXACTAMENTE con mis síntomas y de hecho he resuelto la causa.

Sin duda hay un error en MeasureString AND MeasureCharacterRanges.

La respuesta simple es: Asegúrese de dividir su restricción de ancho (ancho int en MeasureString o la propiedad Size.Width del boundingRect en MeasureCharacterRanges) por 0.72. Cuando obtiene sus resultados de vuelta se multiplican cada dimensión por 0,72 para obtener el resultado VERDADERO

int measureWidth = Convert.ToInt32((float)width/0.72); 
SizeF measureSize = gfx.MeasureString(text, font, measureWidth, format); 
float actualHeight = measureSize.Height * (float)0.72; 

o

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

La explicación (que puedo imaginar) es que algo que ver con el contexto está desencadenando una conversión en los métodos de medición (que no se desencadena en el método DrawString) para inch-> point (* 72/100). Cuando pasa la limitación de ancho REAL está ajustando este valor, por lo que la limitación de ancho MEDIDA es, en efecto, más corta de lo que debería ser. El texto se envuelve antes de lo previsto y, por lo tanto, obtendrá un resultado de mayor altura del esperado. Lamentablemente, la conversión también se aplica al resultado de la altura real, por lo que es una buena idea 'desconvertir' ese valor también.

+0

En una nota indirecta REALMENTE molesta, no tengo absolutamente ninguna idea de por qué MeasureString toma un int para el ancho. Si configura su unidad de medida de contexto de gráficos en pulgadas, entonces (debido a la int) solo puede establecer su límite de ancho en 1 pulgada, o 2 pulgadas, etc. ¡Completamente ridículo! –

Cuestiones relacionadas