2010-04-21 12 views
5

Próbuję renderować tekst do określonej części obrazu w aplikacji Web Forms. Tekst zostanie wprowadzony przez użytkownika, więc chcę zmienić rozmiar czcionki, aby upewnić się, że pasuje do obwiedni.Graphics.MeasureCharacterRanges podając nieprawidłowe obliczenia rozmiaru

Mam kod, który robił to dobrze na mojej implementacji proof-of-concept, ale teraz próbuję go od aktywów od projektanta, które są większe, i otrzymuję dziwne wyniki.

biegnę obliczenia wielkości w następujący sposób:

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

Następnie stosujemy poniższy wiersz do renderowania tekstu na obrazie jestem ciągnięcie z systemu plików:

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

gdzie :

  • __fonts jest PrivateFontCollection,
  • PrivateFontCollection.GetFontBySize jest metoda rozszerzenie, które zwraca FontFamily
  • RectangleF __textBoundingBox = new RectangleF(150, 110, 212, 64);
  • int __minimumSize = 8;
  • int __startingSize = 48;
  • Brush __brush = Brushes.White;
  • int size zaczyna się na 48 i ubytki wewnątrz tej pętli
  • Graphics g ma SmoothingMode.AntiAlias i TextRenderingHint.AntiAlias ustawiony
  • context jest System.Web.HttpContext (jest to fragment z metody z IHttpHandlerProcessRequest)

Inne metody:

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

Teraz dwa problemy.

Po pierwsze, tekst czasami kładzie nacisk na owijanie przez wstawienie łamania linii w słowie, kiedy powinno po prostu nie pasować i spowodować, że pętla while znowu spadnie. Nie rozumiem, dlaczego jest tak, że Graphics.MeasureCharacterRanges uważa, że ​​pasuje to do pudełka, gdy nie powinno się go zawijać w słowie. To zachowanie jest wyświetlane niezależnie od użytego zestawu znaków (otrzymuję go w alfabecie łacińskim, jak również w innych częściach zakresu Unicode, takich jak cyrylica, grecki, gruziński i ormiański). Czy jest jakieś ustawienie, którego powinienem używać, aby wymusić tylko kodowanie słów na znakach białych spacji (lub myślników)? Ten pierwszy problem jest taki sam jak post 2499067.

Po drugie, przy skalowaniu do nowego obrazu i rozmiaru czcionki, Graphics.MeasureCharacterRanges daje mi szaleńczą wysokość. RectangleF Ja rysuję wewnątrz odpowiada wizualnie widocznemu obszarowi obrazu, więc mogę łatwo zobaczyć, kiedy tekst jest zmniejszany bardziej, niż jest to konieczne. Jednak kiedy przekazuję mu trochę tekstu, wywołanie GetBounds daje mi wysokość, która jest prawie dwukrotnie większa od tego, co faktycznie pobiera.

Przy użyciu prób i błędów, aby ustawić __minimumSize wymusić wyjście z pętli while, widzę, że tekst 24pt mieści się w ramce ograniczającej, ale Graphics.MeasureCharacterRanges zgłasza, że ​​wysokość tego tekstu, po renderowaniu do obrazu, ma 122 piksele (gdy ramka ograniczająca ma wysokość 64 pikseli i mieści się w tym polu). Rzeczywiście, bez wymuszania sprawy, pętla while iteruje do 18 punktów, w którym to momencie Graphics.MeasureCharacterRanges zwraca wartość, która pasuje.

Wyciąg dziennika śledzenia jest następujący:

Zmniejszanie rozmiaru czcionki 24, mają wielkość 193 x 122, a granica wynosi 212 x 64
Zmniejszanie rozmiaru czcionki do 23, mają wielkość 191 x 117 i granica wynosi 212 x 64
Zmniejszanie rozmiaru czcionki 22, wielkości 200 x 75, a granica wynosi 212 x 64
Zmniejszanie rozmiar czcionki, 21, jak wielkość wynosi 192 x 71, a granica wynosi 212 x czcionki 64
Zmniejszanie rozmiar do 20, rozmiar 198 × 68 i limit 212 × 64
Decre wykonawczych rozmiar czcionki na 19, a rozmiar to 185 x 65 a granica to 212 x 64
Pisanie Vennegoor of Hesselink w DIN-Black na 18pt, rozmiar jest 178 × 61 a granica to 212 x 64

Więc dlaczego to jest Graphics.MeasureCharacterRanges, co daje zły wynik? Mogłem zrozumieć, że jest to, powiedzmy, wysokość linii czcionki, jeśli pętla zatrzymała się około 21pt (co byłoby wizualnie dopasowane, jeśli zrzutu ekranu wyników i zmierzyć go w Paint.Net), ale idzie daleko dalej niż powinno być robi ponieważ, szczerze mówiąc, zwracają one błędne wyniki.

+0

+1 za jedno z najbardziej udokumentowanych pytań, jakie widziałem od dłuższego czasu! – SouthShoreAK

Odpowiedz

0

Czy możesz spróbować usunąć następujący wiersz?

fmt.FormatFlags = StringFormatFlags.NoClip; 

zwisających części glifĂłw i rozpakowany tekst sięgające poza formatowania prostokąta mogą programu. Domyślnie wszystkie fragmenty tekstu i glifów wychodzące poza prostokąt formatowania są obcięte.

To najlepsze, co mogę wymyślić dla tego :(

+0

Dziękujemy za zamieszczenie odpowiedzi. Spojrzałem na to, sądząc, że to może być problem, ale podana wysokość jest prawie dwukrotnie większa od rzeczywistej wysokości, więc nie sądzę, żeby tak było. Wydaje się, że różnica polega na tym, że StringFormatFlags.NoClip sprawia, że ​​jeśli miseczka litery P (na przykład) właśnie wyskakuje poza obwiednię, wówczas można ją renderować, zamiast ją przycinać. To nie wydaje się być problemem, którego doświadczam. Ale dzięki: o) –

0

Miałem również pewne problemy z metodą MeasureCharacterRanges. To daje mi rozmiary niespójne dla tego samego łańcucha, a nawet samą Graphics obiekt. Wtedy Odkryłem, że zależy to od wartości parametru layoutRect - nie rozumiem, dlaczego, moim zdaniem jest to błąd w kodzie .NET.

Na przykład, jeśli layoutRect był całkowicie pusty (wszystkie wartości ustawiono na zero) , Mam poprawne wartości ciągu "a" - rozmiar był {Width=8.898438, Height=18.10938} za pomocą 12 punktów Ms Sans Serif czcionka.

Jednak, gdy ustawiłem wartość własności "X" prostokąta na liczbę niecałkowitą (jak 1,2), dało mi to {Width=9, Height=19}.

Więc naprawdę myślę, że jest błąd, gdy używasz prostokąta układu z niecałkowitą współrzędną X.

+0

Interesujące - wygląda na to, że zawsze zaokrąglasz swój rozmiar. Niestety, mój layoutRect jest zawsze integralny - jest zdefiniowany jako prywatny statyczny readonly RectangleF __textBoundingBox = new RectangleF (150, 110, 212, 64); i jego wartość nigdy się nie zmienia (oczywiście, ponieważ jest oznaczona jako przeczytana). To zdecydowanie błąd w kodzie .Net, ale nie wygląda na to, że Twój błąd i mój błąd są takie same. –

1

Mam podobny problem. Chcę wiedzieć, jak duży będzie tekst, który rysuję, a gdzie dokładnie się pojawi, DOKŁADNIE. Nie miałem problemu z podziałem linii, więc nie sądzę, abym mógł ci tam pomóc.Miałem takie same problemy z wszystkimi dostępnymi technikami pomiarowymi, w tym z MeasureCharacterRanges, które działały dobrze dla lewej i prawej strony, ale nie dla wysokości i góry. (Gra z linią podstawową może dobrze działać w niektórych rzadkich aplikacjach).

Skończyło się na bardzo nieeleganckim, nieefektywnym, ale działającym rozwiązaniu, przynajmniej w moim przypadku użycia. Rysuję tekst na bitmapie, sprawdzam bity, żeby zobaczyć, gdzie się znalazły, i to jest mój zasięg. Ponieważ w większości rysuję małe czcionki i krótkie ciągi, to było wystarczająco szybkie dla mnie (zwłaszcza przy dodanej memoizacji). Może to nie będzie dokładnie to, czego potrzebujesz, ale może to i tak doprowadzi cię do właściwego toru.

Uwaga: wymaga to kompilacji projektu, aby zezwolić na niebezpieczny kod w tej chwili, ponieważ próbuję wycisnąć z niego każdą część wydajności, ale to ograniczenie może zostać usunięte, jeśli chcesz. Ponadto, nie jest tak bezpieczny jak wątek, jak może być teraz, można go łatwo dodać, jeśli jest to potrzebne.

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

Dobra robota! Muszę spróbować i zobaczyć, czy to rozwiąże mój problem. Chociaż trochę nie lubię polegać na "niebezpiecznym" kodzie. Jak szybko znalazłeś ten kod?Okoliczność, w której go używam, sprawdza, czy tekst jest za duży dla ramki o stałym rozmiarze i zmniejsza rozmiar czcionki w pętli 'while' (sprawdzanie' while too large lub size gt hard-floor-limit'), więc nie chcę wykonywać kosztownej operacji w tej pętli 'while' ... –

+0

Prędkość zależy w dużej mierze od rozmiaru czcionki. Przydziela bitmapę, renderuje ją, a następnie wyszukuje granice. Mniejsze czcionki są o wiele szybsze, a przegrywasz z większymi czcionkami, na przykład z O (rozmiar^2). Może chcieć zgadywać małe rozmiary i pracować do rozmiaru, który jest zbyt duży, zamiast na odwrót. Możesz użyć MeasureCharacterRanges jako pierwszego odgadnięcia dla dolnej granicy, ponieważ zawsze wydaje się, że kończy się zbyt małym odgadywaniem. Jest dużo miejsca na spryt, szukając odpowiedniego rozmiaru, szukania binarnego ze sprytnym odgadywaniem itd. Nie potrzebuję tego dla mojego celu, więc nie grałem z nim. – user12861

+0

Co do niebezpieczeństwa, możesz to zrobić bez tego, ale prędkość przynosi trafienie. Jak zauważyłem, jest to prawie 90% szybsze niż przy użyciu GetPixel, ale nadal można wykonać pomiędzy metodami za pomocą blokad bez niebezpiecznych kodów. Nie próbowałem tego, ponieważ byłem w porządku z niebezpiecznym kodem i chciałem wydobyć trochę łatwo osiągalnych wyników. Istnieje wiele sposobów na wypróbowanie wydajności w zależności od tego, jak dokładnie używasz tych rzeczy, ale wszystkie są skomplikowane. Cała ta rzecz jest trochę nieprzyjemna, w pełni zdaję sobie z tego sprawę. Sądzę, że byłby lepszy sposób, ale moje wyszukiwanie online było równie nieproduktywne jak twoje. – user12861

0

OK, więc 4 lata późno, ale to pytanie DOKŁADNIE pasuje do moich symptomów i faktycznie wypracowałem przyczynę.

Z pewnością występuje błąd w MeasureString AND MeasureCharacterRanges.

Najprostsza odpowiedź to: Upewnij się, że podzielisz ograniczenie szerokości (szerokość int w MeasureString lub właściwość Size.Width obiektu boundingRect w MeasureCharacterRanges) o 0,72. Po otrzymaniu wyników z powrotem pomnożyć każdego wymiaru przez 0,72, aby uzyskać prawdziwy efekt

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

lub

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

Wyjaśnienie (że mogę wymyślić) jest to, że ma coś wspólnego z kontekstem jest wyzwalanie konwersja w metodach Measure (która nie wyzwala w metodzie DrawString) dla cala-> punktu (* 72/100). Po przejściu w RZECZYWISTYM ograniczeniu szerokości wartość ta jest dostosowywana, dlatego ograniczenie szerokości mierzonej jest w efekcie krótsze niż powinno. Twój tekst jest następnie owijany wcześniej niż powinien, aby uzyskać dłuższy wynik niż oczekiwano. Niestety konwersja dotyczy rzeczywistego wyniku wysokości, więc dobrze jest również "konwertować" tę wartość.

+0

Na NAPRAWDĘ denerwującą notatkę, nie mam absolutnie żadnego pojęcia dlaczego MeasureString ma int dla szerokości. Jeśli ustawisz jednostkę miary kontekstu graficznego na cale, wówczas (ze względu na int) możesz ustawić ograniczenie szerokości tylko na 1 cal lub 2 cale itd. Całkowicie śmieszne! –