-
-
Notifications
You must be signed in to change notification settings - Fork 615
HWP Tab Leader Rendering
원본 분석: @seanshin (Shin hyoun mouk) — PR #282 추가 보강: 메인테이너 — Task #279
HWP 의 탭 리더 (Tab Leader) 는 목차/색인 등에서 항목 텍스트와 페이지번호 사이를 시각적으로 연결하는 채움 기호 (점선/실선/파선/쇄선) 다.
Ⅰ. 사업 개요 ················· 3 ← 점선 리더
1. 추진배경 및 목적 ··········· 4
TabDef.tabs[i] = {
position : UINT32 (HWPUNIT) // 탭 정지 위치 (column-relative)
tab_type : UINT8 // 0=LEFT, 1=RIGHT, 2=CENTER, 3=DECIMAL
fill_type: UINT8 // 0=없음, 1=실선, 2=파선, 3=점선, 4~6=쇄선/이중선
}
ext[2] = (tab_type << 8) | fill_type
즉 high byte = tab_type, low byte = fill_type
rhwp 는 fill_type 에 따라 다음과 같이 SVG <line> 으로 렌더한다:
| fill_type | 의미 | SVG 표현 |
|---|---|---|
| 0 | 없음 | (생략) |
| 1 | 실선 | <line stroke-width="0.5"/> |
| 2 | 파선 (- - -) | dasharray="3 3" |
| 3 | 점선 (· · ·) |
stroke-width="1.0" dasharray="0.1 3" stroke-linecap="round" ← 원형 점
|
| 4 | 일점쇄선 | dasharray="6 2 1 2" |
| 5 | 이점쇄선 | dasharray="6 2 1 2 1 2" |
| 6 | 긴파선 | dasharray="8 4" |
이전 (잘못된) 구현은 dasharray="1 2" stroke-width="0.5" 로 표현했는데, 이는 사각 끝의 짧은 대시선 처럼 보였다. 한컴은 점선 리더를 원형 점 으로 표현하므로 다음이 정답:
<line stroke="..." stroke-width="1.0"
stroke-dasharray="0.1 3"
stroke-linecap="round"/>-
stroke-width="1.0": 점의 직경 -
stroke-dasharray="0.1 3": 길이 0.1 의 점 + 간격 3 → 점이 거의 0 길이라 round cap 으로 인해 원형으로 보임 -
stroke-linecap="round": 사각 끝을 둥글게 만들어 점 모양 완성
self.ctx.set_line_cap("round");
draw_line(&self.ctx, ly, 1.0, &[0.1, 3.0]);
self.ctx.set_line_cap("butt"); // 후속 leader 영향 방지HWP 스펙은 데이터 포맷 정의일 뿐, 한컴 조판 알고리즘은 비공개다.
스펙대로 처리하면 한컴과 다르게 보인다. 한컴 시각 결과를 정답으로 삼아 의도를 역공학해야 한다.
TabDef.position 은 절대 좌표가 아닌 힌트 다. 한컴은 다음 컨텍스트로 position 을 시멘틱 재해석한다:
들여쓰기 (margin_left > 0) 가 있는 문단도 페이지번호 right edge 가 동일 위치에 정렬된다. 한컴은 tab_pos 를 그대로 사용하지 않고 단/셀 inner content 우측 끝 으로 보정.
let effective_pos = if tab_type == 1 && fill_type != 0 {
// 리더 있는 RIGHT — inner 우측 끝으로 강제
effective_margin_left + available_width
} else {
tab_pos
};셀 안 paragraph 의 col_area = inner_area = cell - (pad_left, pad_right) 가 이미 padding 적용된 영역. effective_margin_left + available_width 가 자동으로 cell padding_right 영역을 침범하지 않도록 한다.
한 자리/두 자리 페이지번호 무관 leader 끝이 같은 x 라면 leader 가 페이지번호와 겹친다. 한컴은 페이지번호 폭에 따라 leader 끝을 페이지번호 시작 직전까지 단축한다.
rhwp 는 cross-run RIGHT 정렬 시점에 직전 leader-bearing TextRun 의 tab_leaders.last().end_x 를 페이지번호 시작 x − space_gap 으로 보정한다.
목차 장제목은 CharShape 변화로 "...\t" + " " + "3" 처럼 분할된다. (공백) 단독 run 에 RIGHT 정렬을 적용하면 페이지번호 "3" 이 공백 폭만큼 밀린다. 공백 only run 은 정렬 단위가 아니므로 pending 을 다음 의미있는 run 으로 carry-over.
if (tab_type == 1 || tab_type == 2) && run.text.trim().is_empty() {
pending_right_tab = Some((tab_pos, tab_type, fill_type)); // carry-over
} else {
// 정렬 적용
}장제목 두 자리 케이스 (" 16" 한 run) 에서 trim_start 후 폭으로 정렬하면 draw_text 가 공백 포함 텍스트를 그려 페이지번호 첫 글자가 +공백폭 우측에 출력된다. next_w 는 trim 하지 않은 전체 run 폭 으로 사용해야 페이지번호 right edge 가 effective_pos 에 정확히 정렬된다.
samples/KTX.hwp 두 번째 페이지 (목차) 가 본 영역의 정답지. 모든 페이지번호 right edge ≈ 700.0 으로 정렬되어야 한다:
| 행 | 페이지번호 | 첫 글자 x | right edge |
|---|---|---|---|
| Ⅰ. 사업 개요 (장제목) | 3 | 690.76 | ~700.09 |
| 1. 추진배경 및 목적 (소제목) | 4 | 690.76 | ~700.09 |
| Ⅱ. ... (장제목) | 8 | 690.76 | ~700.09 |
| 1. ... (소제목, 두 자리) | 14 | 680.76 | ~699.76 |
| Ⅲ. ... (장제목, 두 자리) | 16 | 681.09 | ~700.43 |
소제목 들여쓰기 (margin_left=24.41) 와 무관하게 페이지번호 right edge 가 장제목과 동일 정렬.
- 이슈: #279
- PR: #282
- 트러블슈팅:
mydocs/troubleshootings/toc_leader_right_tab_alignment.md - HWP Spec Errata: entry 30 —
mydocs/tech/hwp_spec_errata.md - 코드 위치:
-
src/renderer/svg.rs::draw_text(fill_type=3 분기) -
src/renderer/web_canvas.rs::draw_leader(fill_type=3 분기) -
src/renderer/layout/text_measurement.rs::find_next_tab_stop(RIGHT 클램핑 제외) -
src/renderer/layout/paragraph_layout.rs::resolve_last_tab_pending(cross-run pending)
-