Text Height Calculation
WPF FormattedText 기반의 정확한 텍스트 높이 계산 기능 가이드입니다. 레거시 .NET 4.5 편집기와의 완벽한 호환성을 유지하며 다양한 폰트, 크기, 스타일을 지원합니다.
개요
| 특징 | 설명 |
|---|---|
| 레거시 호환성 | .NET 4.5 편집기와 동일한 측정 규칙 적용 |
| WPF 기반 | Windows 렌더링 엔진을 사용한 정확한 측정 |
| 다국어 지원 | 한글, 영문 등 모든 폰트 지원 |
| 고성능 | STA 스레드 풀을 통한 동시 처리 (기본 4개 스레드) |
| 안정성 | 타임아웃 및 과부하 방지 메커니즘 |
레거시 측정 규칙
| 규칙 | 값 | 설명 |
|---|---|---|
| 폭 인셋 | width - 8 | 텍스트 프레임 폭에서 8px 제외 |
| 라인 높이 | fontSize × 1.6984 | 고정 배수 적용 (px 단위) |
| 정렬 방식 | justify | 양쪽 정렬 강제 |
| 최종 높이 | Max(oneLine, total + 10) | 최소 1줄 높이 보장, 전체 높이에 10px 패딩 |
| 폰트 크기 | fontSize × 1.1 | WPF 내부 처리용 |
TextUtil API
GetHeight()
텍스트의 계산된 높이를 반환합니다.
csharp
double height = TextUtil.GetHeight(
text: "안녕하세요\n여러 줄의\n텍스트입니다",
fontSize: 20,
width: 300,
fontFamily: "Malgun Gothic",
lineHeight: "normal",
textAlign: "justify",
bold: false,
italic: false
);GetLineCount()
텍스트가 차지하는 줄 수를 반환합니다.
csharp
int lines = TextUtil.GetLineCount(
text: "긴 텍스트가 여기에 들어갑니다",
fontSize: 16,
width: 250,
fontFamily: "카페24 당당해"
);GetOneLineHeight()
한 줄의 높이를 반환합니다 (텍스트 내용과 무관).
GetDispartText()
주어진 높이 제한 내에서 텍스트를 분할합니다.
csharp
List<string> parts = TextUtil.GetDispartText(
text: "매우 긴 텍스트 내용...",
fontSize: 14,
height: 100,
width: 200,
fontFamily: "맑은 고딕"
);
// parts[0]: 100px 높이 내에 들어가는 텍스트
// parts[1]: 나머지 텍스트 (있는 경우)BadTextRemover()
HTML 엔티티 및 특수문자를 제거합니다.
csharp
string cleaned = TextUtil.BadTextRemover("<div>텍스트 & 내용</div>");
// 결과: "텍스트 & 내용"Test API
POST /books/{bookUid}/text-height
텍스트 높이 계산을 테스트하고 결과를 페이지로 추가하는 API입니다. 개발/디버깅 용도로 사용합니다.
json
// 요청 본문
{
"templateUid": "template-blank-page",
"textJson": [
{
"type": "text",
"position": {"x": 100, "y": 200},
"width": 300,
"height": 0,
"text": "측정할 텍스트\n여러 줄 가능",
"fontFamily": "Malgun Gothic",
"fontSize": 20,
"bold": false,
"italic": false
}
]
}
// 응답
{
"success": true,
"data": {
"pageNumber": 5,
"calculatedHeights": [85.5, 62.3]
},
"message": "Text height page added"
}폰트 처리
텍스트 스타일(굵기, 기울임)은 템플릿 JSON의 textBold와 textItalic 프로퍼티를 통해 적용됩니다.
폰트 파일을
/fonts/ 디렉토리에 넣는 것만으로는 충분하지 않습니다. 반드시 시스템에 설치해야 합니다 (Windows: C:\Windows\Fonts\).프로덕션 환경 고려사항
스레드 풀 설정
bash
# Windows
set WPF_TEXT_MEASURER_THREADS=8
# Linux (Docker)
ENV WPF_TEXT_MEASURER_THREADS=8
# 권장 설정:
# 낮은 부하: 2~4 스레드
# 중간 부하: 4~8 스레드
# 높은 부하: 8~16 스레드타임아웃 설정
| 항목 | 값 |
|---|---|
| 큐 추가 타임아웃 | 2초 |
| 측정 타임아웃 | 10초 |
| 대기열 용량 | 1,000개 |
성능 벤치마크
| 작업 | 평균 시간 | 동시 처리 |
|---|---|---|
| 단일 텍스트 측정 (1줄) | 5ms | 4 스레드 |
| 단일 텍스트 측정 (10줄) | 8ms | 4 스레드 |
| 배치 측정 (10개) | 45ms | 4 스레드 |
| 최대 처리량 | ~800 req/s | 8 스레드 |
제약사항
Windows 전용: WPF 의존으로 Linux에서 동작하지 않습니다. Docker 사용 시 Windows 컨테이너가 필요합니다. 크로스 플랫폼 지원이 필요한 경우 SkiaSharp 또는 SixLabors.Fonts로 전환을 고려하세요.
정확도 관련: WPF와 브라우저의 렌더링 엔진이 다르므로 약간의 차이가 발생할 수 있습니다. 정확한 측정을 위해 동일한 폰트가 시스템에 설치되어 있어야 합니다. DPI는 96으로 고정됩니다.
텍스트 분할 구현
이진 탐색 알고리즘
원본 LinkedBooks의 선형 탐색(O(n)) 대신 이진 탐색(O(log n))을 사용하여 분할 지점을 찾습니다.
| 텍스트 길이 | 이진 탐색 | 선형 탐색 | 성능 향상 |
|---|---|---|---|
| 100자 | 7번 | 최대 100번 | 14배 |
| 500자 | 9번 | 최대 500번 | 55배 |
| 1000자 | 10번 | 최대 1000번 | 100배 |
| 10000자 | 14번 | 최대 10000번 | 714배 |
단어 경계 조정 우선순위
이진 탐색으로 찾은 분할 지점을 자연스러운 위치로 조정합니다.
- 공백 문자 (` `, `\t`) — 단어 중간에서 자르지 않음. 개행(`\n`)은 제외 (문단 간격 보존)
- 문장 부호 (`.`, `!`, `?`, `,`, `。`) — 문장 단위로 자름
- 최소 95% 유지 — 너무 적은 텍스트가 배치되지 않도록
- 원본 지점 — 좋은 지점을 못 찾으면 이진 탐색 결과 사용
개행 처리 방식
text
원본: "상상력을 발휘했어요.\n\n두 번째 전시실에는..."
분할 후:
첫 번째: "상상력을 발휘했어요." (TrimEnd)
두 번째: "\n두 번째 전시실에는..." (앞 개행 1개만 제거, 문단 간격 유지)
// "\n\n두 번째..." → "\n두 번째..." (문단 간격 유지)
// "\n다음 문장" → "다음 문장" (바로 시작)원본(LinkedBooks)과의 차이점 요약
| 항목 | 원본 (LinkedBooks) | 현재 (PhotobookAPI) |
|---|---|---|
| 분할 알고리즘 | 선형 탐색 O(n) | 이진 탐색 O(log n) |
| 폰트 지원 | 고정 (Malgun Gothic) | 다양한 폰트/스타일 |
| 공백 처리 | 항상 Trim() 제거 | trimWhitespace 옵션으로 제어 |
| 분할 정확도 | 단어 경계만 | 문장 > 단어 > 문자 |
| 500자 처리 시간 | ~2,500-4,000ms | ~50-80ms |