2010-04-21 80 views
5

Sto provando a visualizzare del testo in una parte specifica di un'immagine in un'app Web Form. Il testo verrà inserito dall'utente, quindi voglio variare la dimensione del carattere per assicurarmi che si adatti al riquadro di delimitazione.Graphics.MeasureCharacterRange che forniscono calcoli di dimensioni errate

Ho codice che stava facendo questo bene sulla mia implementazione proof-of-concept, ma ora sto provando contro le risorse del designer, che sono più grandi, e sto ottenendo alcuni risultati strani.

sto correndo il calcolo formato come segue:

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

allora io uso la seguente riga per il rendering del testo su un'immagine sto tirando dal filesystem:

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

dove :

  • __fonts è un PrivateFontCollection,
  • PrivateFontCollection.GetFontBySize è un metodo di estensione che restituisce un FontFamily
  • RectangleF __textBoundingBox = new RectangleF(150, 110, 212, 64);
  • int __minimumSize = 8;
  • int __startingSize = 48;
  • Brush __brush = Brushes.White;
  • int size inizia a 48 e decrementi all'interno di tale ciclo
  • Graphics g ha SmoothingMode.AntiAlias e TextRenderingHint.AntiAlias impostare
  • context è una System.Web.HttpContext (questo è un estratto dal metodo di un IHttpHandlerProcessRequest)

Gli altri metodi sono:

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

ora ho due problemi.

In primo luogo, il testo a volte insiste sul wrapping inserendo un'interruzione di riga all'interno di una parola, quando dovrebbe non riuscire ad adattarsi e causare il decremento del ciclo while. Non riesco a capire perché sia ​​lo Graphics.MeasureCharacterRanges a pensare che ciò rientri nella scatola quando non dovrebbe essere il word-wrapping all'interno di una parola. Questo comportamento viene mostrato indipendentemente dal set di caratteri utilizzato (lo trovo nelle parole dell'alfabeto latino, così come in altre parti della gamma Unicode, come il cirillico, il greco, il georgiano e l'armeno). C'è qualche impostazione che dovrei usare per forzare Graphics.MeasureCharacterRanges solo per fare il wordwrap nei caratteri di spazi (o trattini)? Questo primo problema è lo stesso di post 2499067.

In secondo luogo, nel ridimensionare la nuova immagine e la dimensione del carattere, Graphics.MeasureCharacterRanges mi sta dando altezze che sono selvaggiamente fuori. Il disegno RectangleF all'interno corrisponde ad un'area visivamente apparente dell'immagine, così posso facilmente vedere quando il testo viene decrementato più del necessario. Eppure quando gli passo un po 'di testo, la chiamata GetBounds mi dà un'altezza che è quasi il doppio di quello che effettivamente sta prendendo.

Utilizzo di tentativi ed errori per impostare __minimumSize per forzare un'uscita dal ciclo while, posso vedere che il testo 24pt si inserisce all'interno del riquadro di delimitazione, tuttavia Graphics.MeasureCharacterRanges sta segnalando che l'altezza di quel testo, una volta renderizzata all'immagine, è 122px (quando il riquadro di delimitazione è alto 64 px e rientra in tale riquadro). In effetti, senza forzare la questione, il ciclo while itera fino a 18pt, a quel punto Graphics.MeasureCharacterRanges restituisce un valore che si adatta.

Il brano registro di traccia è il seguente:

decremento dimensione del carattere a 24, come la dimensione è 193 × 122 e il limite è 212 × 64
decremento dimensione del carattere a 23, come la dimensione è 191 × 117 e il limite è 212 × 64
di decremento dimensione di carattere 22, la dimensione è 200 × 75 e il limite è 212 × 64
Decrementale dimensione di carattere 21, la dimensione è 192 × 71 e il limite è 212 × carattere 64
Decrementale dimensione a 20, poiché la dimensione è 198 × 68 e il limite è 212 × 64
Decre Menting dimensione del carattere a 19, come la dimensione è 185 × 65 ed il limite è 212 × 64
Scrittura Vennegoor of Hesselink DIN-Black a 18pt, la dimensione è 178 × 61 e il limite è 212 × 64

Allora perché è Graphics.MeasureCharacterRanges mi dà un risultato sbagliato? Potrei capire che è, ad esempio, l'altezza della linea del font se il loop si fermava intorno a 21pt (che sarebbe visivamente in forma, se faccio uno screenshot dei risultati e lo misuro in Paint.Net), ma sta andando molto più lontano di quanto dovrebbe fare perché, francamente, sta restituendo i dannati risultati sbagliati.

+0

+1 per una delle domande più documentate che ho visto da un po '! – SouthShoreAK

risposta

0

Provare a rimuovere la riga seguente?

fmt.FormatFlags = StringFormatFlags.NoClip; 

parti debordanti di glifi e testo scartato portata fuori del formattazione rettangolo possono spettacolo. Per impostazione predefinita, tutto il testo e glifi parti che raggiungono il rettangolo di formattazione vengono tagliati.

Questo è il meglio che posso venire con per questo :(

+0

Grazie per aver postato una risposta. Ho dato un'occhiata a questo, avendo pensato che potesse essere il problema, ma l'altezza riportata è quasi il doppio dell'altezza attuale, quindi non credo che possa essere così. Sembra che la differenza StringFormatFlags.NoClip faccia è che se la ciotola di una lettera P (per esempio) sporge appena fuori dal riquadro di delimitazione, allora è permesso di renderizzare, piuttosto che essere ritagliato. Questo non sembra essere il problema che sto vivendo. Ma grazie: o) –

0

Ho anche avuto alcuni problemi con il metodo MeasureCharacterRanges. Mi stava dando dimensioni incoerenti per la stessa stringa e anche lo stesso Graphics oggetto. Poi ho scoperto che dipende dal valore della layoutRect parametr - non riesco a capire perché, a mio parere si tratta di un bug nel codice .NET

per esempio, se layoutRect era completamente vuoto (tutti i valori impostati a zero). , Ho ottenuto i valori corretti per la stringa "a" - la dimensione era {Width=8.898438, Height=18.10938} utilizzando 12pt Carattere Ms Sans Serif.

Tuttavia, quando ho impostato il valore della proprietà 'X' del rettangolo su un numero non intero (come 1.2), mi ha dato {Width=9, Height=19}.

Quindi penso davvero che ci sia un bug quando si utilizza un rettangolo di layout con coordinate X non intero.

+0

Interessante: sembra che stia sempre arrotondando le dimensioni. Sfortunatamente, il mio layoutRect è sempre integrale - è definito come privato statico readonly RectangleF __textBoundingBox = new RectangleF (150, 110, 212, 64); e il suo valore non cambia mai (ovviamente, dato che è contrassegnato in sola lettura). È sicuramente un bug nel codice .Net, ma non sembra il tuo bug e il mio bug è lo stesso. –

1

Ho un problema simile. Voglio sapere quanto sarà grande il testo che sto disegnando e dove verrà visualizzato, ESATTAMENTE. Non ho avuto il problema di interruzione di linea, quindi non credo di poterti aiutare.Ho avuto gli stessi problemi con tutte le varie tecniche di misurazione disponibili, incluso finire con MeasureCharacterRanges, che funzionava bene per sinistra e destra, ma non per l'altezza e la parte superiore. (Giocare con la linea di base può funzionare bene per alcune applicazioni rare.)

Ho finito con una soluzione poco elegante, inefficiente, ma funzionante, almeno per il mio caso d'uso. Disegna il testo su una bitmap, controllo i bit per vedere dove sono finiti e questa è la mia portata. Dal momento che principalmente disegno piccoli caratteri e stringhe brevi, è stato abbastanza veloce per me (specialmente con la memoizzazione che ho aggiunto). Forse questo non sarà esattamente quello di cui hai bisogno, ma forse può portarti comunque sulla giusta strada.

Nota che è necessario compilare il progetto per consentire codice non sicuro al momento, poiché sto cercando di spremere fuori ogni bit di efficienza da esso, ma tale restrizione potrebbe essere rimossa se lo si desidera. Inoltre, non è thread-safe come potrebbe essere in questo momento, si potrebbe facilmente aggiungere che se ne avesse avuto bisogno.

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

Bel lavoro! Dovrò fare una prova e vedere se questo risolve il mio problema. Anche se sono un po 'dispiaciuto dover fare affidamento sul codice 'non sicuro'. Quanto velocemente trovi quel codice?La circostanza che sto usando per verificare se il testo è troppo grande per un riquadro di delimitazione a dimensione fissa e diminuisce la dimensione del carattere all'interno di un ciclo 'while' (controllando' mentre troppo grande o dimensione gt hard-floor-limit'), quindi non voglio eseguire un'operazione costosa in quel ciclo 'while' ... –

+0

La velocità dipende fortemente dalle dimensioni del carattere. Alloca bitmap, lo esegue, quindi cerca i limiti. I caratteri più piccoli sono molto più veloci e, a mio avviso, perdi con font più grandi, come O (size^2). Potrebbe voler indovinare piccole dimensioni e lavorare su una dimensione che è troppo grande invece del contrario. È possibile utilizzare MeasureCharacterRanges come prima ipotesi per il limite inferiore, poiché sembra che finisca sempre per indovinare. C'è un sacco di spazio per l'intelligenza alla ricerca delle giuste dimensioni, ricerca binaria con intelligente congettura ecc. Non ho bisogno di nulla per il mio scopo, quindi non ho giocato con esso. – user12861

+0

Per quanto riguarda la sicurezza, è possibile farlo senza che la velocità subisca un colpo. Come commentato, questo è quasi il 90% più veloce rispetto all'utilizzo di GetPixel, ma è comunque possibile eseguire un metodo intermedio usando LockBits senza codice non sicuro. Non l'ho provato perché stavo bene con il codice non sicuro e volevo spremere un po 'di prestazioni facilmente ottenute. Ci sono molti modi per provare a spremere le prestazioni a seconda di come usi esattamente questa roba, ma sono tutte complicate. L'intera faccenda è abbastanza cattiva che realizzo pienamente. Penserei che ci sarebbe un modo migliore, ma la mia ricerca online è stata improduttiva come la tua. – user12861

0

Ok 4 anni di ritardo ma questa domanda corrispondeva ESATTAMENTE ai miei sintomi e in realtà ho risolto la causa.

C'è sicuramente un bug in MeasureString AND MeasureCharacterRanges.

La risposta semplice è: Assicurarsi di dividere la restrizione della larghezza (larghezza int in MeasureString o la proprietà Size.Width del boundingRect in MeasureCharacterRanges) per 0,72. Quando si ottiene i risultati indietro moltiplicare ogni dimensione da 0,72 per ottenere il risultato REALE

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 spiegazione (che posso capire) è che qualcosa a che fare con il contesto sta innescando una conversione nei metodi Measure (che non si innesca nel metodo DrawString) per inch-> point (* 72/100). Quando si passa alla limitazione della larghezza EFFETTIVA, questo valore viene regolato in modo che la limitazione della larghezza MISURATA sia, in effetti, più breve di quanto dovrebbe essere. Il testo viene quindi avvolto prima di quanto previsto e quindi si ottiene un risultato di altezza più lungo del previsto. Sfortunatamente la conversione si applica anche al risultato dell'altezza effettiva, quindi è una buona idea "non convertire" anche quel valore.

+0

Su una nota laterale DAVVERO fastidiosa, non ho assolutamente alcuna idea del perché MeasureString prende un int per larghezza. Se si imposta l'unità di misura del contesto grafico in pollici, (a causa dell'int) è possibile impostare solo la limitazione della larghezza su 1 pollice, o 2 pollici, ecc. Completamente ridicolo! –