2010-04-21 2 views
5

Web Forms 응용 프로그램에서 이미지의 특정 부분에 텍스트를 렌더링하려고합니다. 텍스트는 사용자가 입력하므로 글꼴 상자를 테두리 상자에 맞도록 글꼴 크기를 변경하고 싶습니다.잘못된 크기 계산을하는 Graphics.MeasureCharacterRanges

개념 증명 구현에서이 작업을 수행하는 코드가 있지만 디자이너의 자산에 비해 더 큰 크기의 애셋을 사용하려고 노력 중입니다. 이상한 결과가 나타납니다. 다음과 같이

나는 크기 계산을 실행 해요 :

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
  • ,369 PrivateFontCollection입니다 1,363,210
  • PrivateFontCollection.GetFontBySizeFontFamily
  • RectangleF __textBoundingBox = new RectangleF(150, 110, 212, 64);
  • int __minimumSize = 8;
  • int __startingSize = 48;
  • Brush __brush = Brushes.White;
  • int size
  • 갖는다 SmoothingMode.AntiAliasGraphics gTextRenderingHint.AntiAlias
  • 설정하는 루프 내 48 아웃 시작 및 감소 리턴 확장 방법
  • contextSystem.Web.HttpContext이다

(이것이 IHttpHandlerProcessRequest 방법에서 발췌) 다른 방법은 :

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

는 이제 두 가지 문제점이있다.

첫 번째로 가끔은 단어 내에 줄 바꿈을 삽입하여 줄 바꿈을 요구합니다. 다만 줄 바꿈이 실패 할 경우에만 while 루프가 다시 감소하게됩니다. 나는 왜 그것이 단어 내에 랩핑해서는 안되는 지 상자 안에 들어 맞다고 생각하는 이유를 알 수 없다. 이 동작은 사용 된 문자 집합과 관계없이 표시됩니다 (키릴 문자, 그리스어, 그루지야 어 및 아르메니아어와 같이 유니 코드 범위의 다른 부분은 물론 라틴 알파벳으로도 알 수 있습니다). Graphics.MeasureCharacterRanges을 공백 문자 (또는 하이픈)에서 단어 줄 바꿈 만 사용하도록 설정해야하는 몇 가지 설정이 있습니까? 이 첫 번째 문제는 post 2499067과 같습니다.

둘째, 새 이미지와 글꼴 크기까지 확대 할 때 Graphics.MeasureCharacterRanges을 사용하면 큰 차이가 있습니다. RectangleF 내에서 그림을 볼 때 이미지의 시각적 영역에 해당하므로 필요한 경우보다 텍스트가 더 많이 감소하는 것을 쉽게 볼 수 있습니다. 그러나 일부 텍스트를 전달할 때 GetBounds 호출은 나에게 실제로 걸리는 높이의 두 배를주고 있습니다.

시도 및 오류를 사용하여 __minimumSize에서 while 루프를 종료하도록 설정하면 경계 상자 내에 24pt 텍스트가 들어가는 것을 볼 수 있습니다. 그러나 Graphics.MeasureCharacterRanges은 한 번 이미지에 렌더링 된 해당 텍스트의 높이가, 122px입니다 (테두리 상자의 크기가 64px이고 상자 안에 들어있는 경우). 실제로 while 루프는 18pt로 반복되고 어떤 점에서는 Graphics.MeasureCharacterRanges이 맞는 값을 반환합니다.

다음 추적 로그 발취된다 같이

24 폰트 크기를 감소시키는 크기는 191 × 117이므로, 크기가 193 × 122이고 한계는 212 × 64
가 23 폰트 크기를 감소시키는대로 크기는 200 × 75이고, 크기는 192 × 71이므로 한도는 212 × 64
크리먼트 폰트 크기가 21이고, 한계 212 × 64
크리먼트 폰트 같이 리미트는 212 × 64
가 22 폰트 크기를 감소시키는 것이다 크기는 20 x 198 크기는 198 x 68이고 제한은 212 x 64
감소 19 menting 폰트 크기, 크기는 185 × 65이고 한계는 212 × 64
18pt에서 DIN-블랙 페네호르 작성 오프 헤셀링크, 크기는 178 × 61이고 한계는 212 × 64

왜이기 때문에 Graphics.MeasureCharacterRanges이 나에게 잘못된 결과를 주는가? 루프가 21pt 주위에 멈추었을 때 글꼴의 선 높이를 이해할 수있었습니다 (결과가 스크린 샷으로 표시되고 Paint.Net에서 측정되는 경우 시각적으로 적합합니다).하지만 수행해야 할 것보다 훨씬 더 진행되고 있습니다. 왜냐하면 솔직히 잘못된 결과를 돌려주고 있기 때문입니다.

+0

내가 잠시 본 문서 중 가장 잘 나온 질문 중 하나입니다. – SouthShoreAK

답변

0

다음 줄을 제거해주세요. 사각형을 서식 외부에 도달 상형 문자의

fmt.FormatFlags = StringFormatFlags.NoClip; 

돌출 된 부분과 미개봉 텍스트가 쇼에 사용할 수 있습니다. 기본적으로 모든 텍스트 및 글리프 부분은 서식 지정 사각형 외부에 도달합니다.

는 최선 즉, 나는이 나는 또한 MeasureCharacterRanges 방법 몇 가지 문제가 있었다

+0

답변을 게시 해 주셔서 감사합니다. 나는 이것이 문제가 될 수 있다고 생각하면서 이것을 보았지만,보고 된 높이는 실제 높이의 거의 두 배에 달한다. 그렇게 될 수는 없다. StringFormatFlags.NoClip의 차이는 문자 P의 보울 (예 :)이 경계 상자 바깥 쪽에서 단지 튀어 나오면 잘리는 것이 아니라 렌더링이 허용된다는 것입니다. 그건 내가 겪고있는 문제가 아닌 것 같다. 감사합니다. o) –

0

:(위해 가지고 올 수 있습니다. 그것은 나에게 동일한 문자열에 대한 일관성 크기와도 같은 Graphics 객체를주고 있었다. 그리고 나는 그것이 layoutRect 매개 변수의 값에 달려 있다는 것을 발견했다. 이유는 알 수 없다. 제 생각에는 .NET 코드의 버그이다.

예를 들어, layoutRect이 완전히 비어 있다면 (모든 값이 0으로 설정된 경우) , 문자열 "a"에 대한 올바른 값을 얻었습니다 - 크기는 {Width=8.898438, Height=18.10938} 12pt를 사용했습니다. Ms Sans Serif 서체.

그러나 사각형의 'X'속성 값을 1.2와 같은 정수가 아닌 숫자로 설정하면 {Width=9, Height=19}이 표시됩니다.

그래서 정수가 아닌 X 좌표로 레이아웃 사각형을 사용할 때 버그가 있다고 생각합니다.

+0

흥미 롭습니다. 항상 크기를 반올림하는 것처럼 보입니다. 불행히도 내 layoutRect는 항상 정수입니다. private static 읽기 전용 RectangleF __textBoundingBox = new RectangleF (150, 110, 212, 64); 값은 변경되지 않습니다 (분명히 읽기 전용으로 표시되어 있음). 확실히 .Net 코드의 버그입니다. 그러나 귀하의 버그와 같지 않고 내 버그가 동일합니다. –

1

비슷한 문제가 있습니다. 제가 그리는 텍스트가 얼마나 커질 지 그리고 정확히 어디에 나타날지 알고 싶습니다. 줄 바꿈 문제가 없었으므로 여기서 도움을 줄 수 있다고 생각하지 않습니다.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

좋은 직장! 그 문제를 해결할 수 있는지 알아봐야 할 것입니다. 나는''안전하지 않은''코드에 의존해야한다는 점에 다소 혐오 스럽지만. 코드를 얼마나 빨리 찾을 수 있습니까?상황에서 나는 텍스트가 고정 크기 경계 상자에 비해 너무 크고'while' 루프 ('too too big or size gt hard-floor-limit'를 확인)에서 폰트 크기를 줄이는 지 확인합니다. 그래서 while 루프에서 값 비싼 연산을 수행하고 싶지는 않습니다 ... –

+0

속도는 글꼴 크기에 크게 의존합니다. 비트 맵을 할당하고 렌더링 한 다음 경계를 검색합니다. 더 작은 폰트는 훨씬 빠르며 O (size^2)와 같은 큰 폰트로는 잃어 버릴 것입니다. 작은 크기를 추측하고 다른 크기가 아닌 너무 큰 크기까지 작업 할 수 있습니다. MeasureCharacterRanges는 항상 하한에 대한 첫 번째 추측으로 사용할 수 있습니다. 이는 항상 너무 작은 추측을 끝내는 것처럼 보이기 때문입니다. 올바른 크기, 영리한 추측 등을 포함한 이진 검색을 찾는 영리함을위한 여지가 많이 있습니다. 제 목적을 위해 그 중 어떤 것도 필요로하지 않으므로 그걸 가지고 놀지 않았습니다. – user12861

+0

안전하지 않은 문제는 속도가 빠르면 빠르지 만 속도는 빠릅니다. 주석으로 말하면 GetPixel을 사용하는 것보다 거의 90 % 빠르지 만 안전하지 않은 코드 없이는 LockBits를 사용하여 중간에서 작업을 수행 할 수 있습니다. 내가 안전하지 않은 코드로 괜찮 았고 쉽게 얻을 수있는 성능을 약간 쥐어 짜고 싶었 기 때문에 그렇게하지 않았습니다. 이 물건을 얼마나 정확하게 사용하고 있는지에 따라 성능을 압박하려고하는 방법은 많이 있지만, 모두 복잡합니다. 이 모든 일은 내가 완전히 깨달은 종류의 일이다. 나는 더 나은 방법이있을 것이라고 생각하지만, 온라인 검색은 당신만큼 비생산적이었습니다. – user12861

0

그래, 4 년 늦었지 만이 질문은 정확히 내 증상과 일치하고 실제로 원인을 찾았습니다.

MeasureString과 MeasureCharacterRanges에는 확실히 버그가 있습니다.

간단한 대답은 : MeasureCharacterRanges에서 boundingRect의 너비 제한 (MeasureString의 int 너비 또는 Size.Width 속성)을 0.72로 나누어야합니다. 당신은 REAL 결과를 얻기 위해 다시 곱 각 차원에 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; 
} 

설명은 (I 알아낼 수) 컨텍스트 함께 할 수있는 뭔가가 트리거된다는 점이다 인치 -> 점 (* 72/100)에 대한 Measure 메서드의 변환 (DrawString 메서드에서 트리거되지 않음) ACTUAL 너비 제한을 전달할 때 MEASURED 너비 제한이 실제보다 짧아 지도록이 값을 조정합니다. 그러면 텍스트가 예상보다 빨리 래핑되므로 예상보다 긴 높이 결과를 얻습니다. 불행하게도 변환은 실제 높이 결과에도 적용되므로 값을 '변환하지 않는'것이 좋습니다.

+0

정말 귀찮은 쪽지에 MeasureString이 너비에 int를 사용하는 이유는 전혀 알지 못합니다. 그래픽 컨텍스트 측정 단위를 인치로 설정하면 (int 값으로 인해) 너비 제한을 1 인치 또는 2 인치 만 설정할 수 있습니다. 완전히 우스운 이야기! –