Skip to content

HWP Tab Leader Rendering

edward edited this page Apr 25, 2026 · 1 revision

HWP 탭 리더(Tab Leader) 렌더링

원본 분석: @seanshin (Shin hyoun mouk) — PR #282 추가 보강: 메인테이너 — Task #279

개요

HWP 의 탭 리더 (Tab Leader) 는 목차/색인 등에서 항목 텍스트와 페이지번호 사이를 시각적으로 연결하는 채움 기호 (점선/실선/파선/쇄선) 다.

Ⅰ. 사업 개요 ················· 3       ← 점선 리더
1. 추진배경 및 목적 ··········· 4

데이터 구조

TabDef (HWP5 스펙 §4.2.7, 표 7)

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=쇄선/이중선
}

tab_extended (HWPX 인라인 탭)

ext[2] = (tab_type << 8) | fill_type
       즉 high byte = tab_type, low byte = fill_type

fill_type 별 SVG 렌더링

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"

점선 (fill_type=3) 의 핵심 — round cap

이전 (잘못된) 구현은 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": 사각 끝을 둥글게 만들어 점 모양 완성

Web Canvas 동등 구현

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 을 시멘틱 재해석한다:

1. 리더 (fill_type ≠ 0) + RIGHT (tab_type=1) → "이 줄 우측 끝까지"

들여쓰기 (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
};

2. 셀 안 paragraph 의 padding_right 인지

셀 안 paragraph 의 col_area = inner_area = cell - (pad_left, pad_right) 가 이미 padding 적용된 영역. effective_margin_left + available_width 가 자동으로 cell padding_right 영역을 침범하지 않도록 한다.

3. 페이지번호 폭별 리더 길이 차등화

한 자리/두 자리 페이지번호 무관 leader 끝이 같은 x 라면 leader 가 페이지번호와 겹친다. 한컴은 페이지번호 폭에 따라 leader 끝을 페이지번호 시작 직전까지 단축한다.

rhwp 는 cross-run RIGHT 정렬 시점에 직전 leader-bearing TextRun 의 tab_leaders.last().end_x 를 페이지번호 시작 x − space_gap 으로 보정한다.

4. 공백 only run carry-over

목차 장제목은 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 {
    // 정렬 적용
}

5. 선행 공백 시각 보정

장제목 두 자리 케이스 (" 16" 한 run) 에서 trim_start 후 폭으로 정렬하면 draw_text 가 공백 포함 텍스트를 그려 페이지번호 첫 글자가 +공백폭 우측에 출력된다. next_w 는 trim 하지 않은 전체 run 폭 으로 사용해야 페이지번호 right edge 가 effective_pos 에 정확히 정렬된다.

검증 — KTX.hwp 목차

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)

Clone this wiki locally