2010-04-21 4 views
5

J'essaie de restituer du texte dans une partie spécifique d'une image dans une application Web Forms. Le texte sera saisi par l'utilisateur, je veux donc faire varier la taille de la police pour m'assurer qu'elle tient dans le cadre.Graphics.MeasureCharacterRanges donnant des calculs de taille incorrecte

J'ai du code qui fonctionnait très bien sur ma mise en œuvre de preuve de concept, mais je l'essaie maintenant par rapport aux ressources du concepteur, qui sont plus grandes, et j'obtiens des résultats étranges.

Je suis en cours d'exécution du calcul de la taille comme suit:

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

J'utilise la ligne suivante pour rendre le texte sur une image je tire du système de fichiers:

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

où :

  • __fonts est un PrivateFontCollection,
  • PrivateFontCollection.GetFontBySize est une méthode d'extension qui retourne un FontFamily
  • RectangleF __textBoundingBox = new RectangleF(150, 110, 212, 64);
  • int __minimumSize = 8;
  • int __startingSize = 48;
  • Brush __brush = Brushes.White;
  • int size commence à 48 et décrémentée dans cette boucle
  • Graphics g a SmoothingMode.AntiAlias et TextRenderingHint.AntiAlias mis
  • context est un System.Web.HttpContext (ce qui est un extrait de la méthode ProcessRequest d'un IHttpHandler)

Les autres méthodes sont:

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

Maintenant, j'ai deux problèmes. Tout d'abord, le texte parfois insiste parfois sur l'encapsulation en insérant un saut de ligne dans un mot, alors que cela devrait échouer et que la boucle while devrait à nouveau être décrémentée. Je ne peux pas voir pourquoi c'est Graphics.MeasureCharacterRanges pense que cela correspond à la boîte quand il ne devrait pas être mot-emballage dans un mot. Ce comportement est exposé quel que soit le jeu de caractères utilisé (je l'obtiens en mots de l'alphabet latin, ainsi que d'autres parties de la gamme Unicode, comme le cyrillique, le grec, le géorgien et l'arménien). Y-a-t-il un réglage que je devrais utiliser pour forcer Graphics.MeasureCharacterRanges à n'entourer que les caractères blancs (ou traits d'union)? Ce premier problème est le même que post 2499067.

Deuxièmement, en augmentant la taille de la nouvelle image et de la police, Graphics.MeasureCharacterRanges me donne des hauteurs qui sont follement éteintes. Le RectangleF Je dessine dedans correspond à une zone visuellement apparente de l'image, donc je peux facilement voir quand le texte est décrémenté plus que nécessaire. Pourtant, quand je lui passe un peu de texte, l'appel GetBounds me donne une hauteur qui est presque le double de ce qu'il prend réellement.En utilisant l'essai et l'erreur pour définir le __minimumSize pour forcer une sortie de la boucle while, je peux voir que le texte 24pt tient dans la boîte englobante, mais Graphics.MeasureCharacterRanges rapporte que la hauteur de ce texte, une fois rendu à l'image, est 122px (lorsque la boîte englobante est de 64px et qu'elle rentre dans cette boîte). En effet, sans forcer la matière, la boucle while se propage à 18pt, point auquel Graphics.MeasureCharacterRanges renvoie une valeur qui convient.

L'extrait de journal de trace est la suivante:

décrémentation taille de police à 24, que la taille est de 193 × 122 et la limite est de 212 × 64
décrémentation taille de police à 23, que la taille est de 191 × 117 et la limite est de 212 × 64
décrémentation taille de police à 22, que la taille est de 200 × 75 et la limite est de 212 × 64
taille de la police décrémentation à 21, que la taille est de 192 × 71 et la limite est de 212 × 64
police Décrémental taille à 20, car la taille est 198 × 68 et la limite est 212 × 64
Decre Menting taille de police à 19, que la taille est de 185 × 65 et la limite est de 212 × 64
écriture Vennegoor de HESSELINK dans la norme DIN-noir à 18 pt, la taille est de 178 × 61 et la limite est de 212 × 64

Alors pourquoi est Graphics.MeasureCharacterRanges en me donnant un mauvais résultat? Je pourrais comprendre, disons, la hauteur de ligne de la police si la boucle s'arrêtait autour de 21pt (ce qui correspondrait visuellement, si je capture les résultats et les mesure dans Paint.Net), mais ça va bien au-delà parce que, franchement, il retourne les mauvais résultats.

+0

+1 pour l'une des questions les plus documentées que j'ai vues depuis un moment! – SouthShoreAK

Répondre

0

Pourriez-vous essayer de supprimer la ligne suivante?

fmt.FormatFlags = StringFormatFlags.NoClip; 

parties surplombs de Glyphes et texte non emballés atteignant en dehors du rectangle mise en forme sont autorisés à spectacle. Par défaut, toutes les parties du texte et du glyphe dépassant le rectangle de mise en forme sont tronquées.

C'est le meilleur que je peux trouver pour ce :(

+0

Merci d'avoir posté une réponse. J'ai regardé ceci, ayant pensé que ce pourrait être le problème, mais la taille rapportée est presque le double de la taille réelle, donc je ne pense pas que cela puisse être cela. Il semble que la différence faite par StringFormatFlags.NoClip est que si le bol d'une lettre P (par exemple) pique juste à l'extérieur du cadre de délimitation, alors il est autorisé à rendre, plutôt que d'être écrêté. Cela ne semble pas être le problème que je rencontre. Mais merci: o) –

0

J'ai aussi eu quelques problèmes avec la méthode MeasureCharacterRanges. Il me donnait des tailles incohérentes pour la même chaîne et même le même objet Graphics. Ensuite, J'ai découvert que cela dépend de la valeur du layoutRect Parametr - Je ne vois pas pourquoi, à mon avis, c'est un bug dans le code .NET

Par exemple, si layoutRect était complètement vide (toutes les valeurs à zéro). , J'ai eu des valeurs correctes pour la chaîne "a" - la taille était {Width=8.898438, Height=18.10938} en utilisant 12pt Ms Sans Serif police.

Cependant, lorsque j'ai défini la valeur de la propriété 'X' du rectangle sur un nombre non entier (comme 1.2), cela m'a donné {Width=9, Height=19}.

Donc je pense vraiment qu'il y a un bug quand vous utilisez un rectangle de disposition avec des coordonnées X non entières.

+0

Intéressant - on dirait qu'il arrondit toujours votre taille. Malheureusement, ma layoutRect est toujours intégrale - elle est définie comme private static readonly RectangleF __textBoundingBox = new RectangleF (150, 110, 212, 64); et sa valeur ne change jamais (évidemment, comme il est marqué en lecture seule). C'est certainement un bug dans le code .Net, mais il ne semble pas que votre bug et mon bug soient les mêmes. –

1

J'ai un problème similaire. Je veux savoir à quel point le texte que je dessine va être, et où il va apparaître, EXACTEMENT. Je n'ai pas eu le problème de la rupture de ligne, donc je ne pense pas pouvoir vous aider là-bas.J'ai eu les mêmes problèmes que vous avez eu avec toutes les différentes techniques de mesure disponibles, y compris finir avec MeasureCharacterRanges, qui a bien fonctionné pour la gauche et la droite, mais pas du tout pour la hauteur et le haut. (Jouer avec la base peut cependant bien fonctionner pour certaines applications rares.)

Je me suis retrouvé avec une solution très inélégante, inefficace, mais fonctionnelle, au moins pour mon cas d'utilisation. Je dessine le texte sur un bitmap, vérifie les bits pour voir où ils se sont retrouvés, et c'est ma gamme. Comme je dessine principalement des petites polices et des chaînes courtes, cela a été assez rapide pour moi (surtout avec la mémo que j'ai ajoutée). Peut-être que ce ne sera pas exactement ce dont vous avez besoin, mais peut-être que cela peut vous conduire sur la bonne voie de toute façon. Notez qu'il faut compiler le projet pour permettre le code dangereux pour le moment, car j'essaye d'en extraire toute l'efficacité, mais cette contrainte pourrait être supprimée si vous le vouliez. De plus, ce n'est pas aussi sûr qu'il pourrait l'être en ce moment, vous pourriez facilement ajouter cela si vous en aviez besoin.

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

Beau travail! Je vais devoir essayer et voir si cela résout mon problème. Bien que je sois un peu déteste de devoir compter sur du code 'dangereux '. À quelle vitesse trouvez-vous ce code?La circonstance que je l'utilise vérifie si le texte est trop grand pour un cadre de taille fixe et diminue la taille de la police dans une boucle while (en vérifiant 'while too big ou size gt hard-floor-limit'), donc je ne veux pas effectuer une opération coûteuse dans cette boucle while while ... –

+0

La vitesse dépend fortement de la taille de la police. Il alloue bitmap, le rend, puis recherche des limites. Les polices plus petites sont beaucoup plus rapides, et vous perdez avec des polices plus grandes quelque chose comme O (taille^2) je pense. Peut vouloir deviner petite taille et travailler jusqu'à une taille qui est trop grande au lieu de l'inverse. Vous pouvez utiliser MeasureCharacterRanges comme première estimation pour la limite inférieure, car elle semble toujours trop petite. Il y a beaucoup de place pour l'intelligence à la recherche de la bonne taille, la recherche binaire avec des devinettes intelligentes, etc. Je n'en ai pas besoin pour mon but, donc je n'ai pas joué avec. – user12861

+0

Quant à dangereux, vous pouvez le faire sans cela, mais la vitesse prend un coup. Comme nous l'avons remarqué, cela est presque 90% plus rapide que l'utilisation de GetPixel, mais vous pouvez toujours faire une méthode intermédiaire en utilisant LockBits sans code dangereux. Je n'ai pas essayé cela parce que j'étais correct avec un code dangereux et que je voulais obtenir un peu de performance facilement obtenue. Il y a beaucoup de façons d'essayer de réduire la performance en fonction de la façon dont vous utilisez exactement ce genre de choses, mais elles sont toutes compliquées. Toute cette affaire est un peu méchant que je réalise pleinement. Je pense qu'il y aurait un meilleur moyen, mais ma recherche en ligne était aussi improductive que la vôtre. – user12861

0

Ok, donc 4 ans de retard, mais cette question correspondait EXACTEMENT à mes symptômes et j'ai effectivement trouvé la cause.

Il y a certainement un bogue dans MeasureString AND MeasureCharacterRanges.

La réponse simple est: Assurez-vous de diviser votre restriction de largeur (largeur int dans MeasureString ou la propriété Size.Width de la boundingRect dans MeasureCharacterRanges) par 0,72. Lorsque vous obtenez vos résultats retour se multiplient chaque dimension par 0,72 pour obtenir le résultat REAL

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

ou

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

L'explication (que je peux comprendre) est que quelque chose à voir avec le contexte déclenche une conversion dans les méthodes Measure (qui ne se déclenche pas dans la méthode DrawString) pour inch-> point (* 72/100). Lorsque vous passez la limitation de largeur ACTUELLE, il ajuste cette valeur de sorte que la limitation de largeur MESURÉE soit, en fait, plus courte qu'elle ne devrait l'être. Votre texte est alors enveloppé plus tôt que prévu et vous obtenez ainsi un résultat de hauteur plus long que prévu. Malheureusement, la conversion s'applique également au résultat de la hauteur réelle. Il est donc recommandé de ne pas convertir cette valeur.

+0

Sur une remarque côté VRAIMENT ennuyeux, je n'ai absolument aucune idée pourquoi MeasureString prend un int pour la largeur. Si vous définissez votre unité de mesure de contexte graphique en pouces alors (en raison de l'int), vous pouvez seulement définir votre limitation de largeur à 1 pouce, ou 2 pouces, etc. Complètement ridicule! –

Questions connexes