증상
samples/exam_math.hwp 페이지 7의 18번 "수열" 문항 첫 줄에서 "수열 {a_n}이 모든 자연수 n에 대하여" 텍스트가 좌측 열의 우측 끝으로 밀려나 렌더링됨.
- PDF (정답):
18. 수열 {a_n}이 모든 ... — "18."과 "수열" 사이는 짧은 공백
- SVG (버그):
18. ······················· 수열 {a_n}이 모든 ... — "수열"이 우측 끝 근처
기타 문항(19, 20번)과 본 문항의 2·3째 줄은 영향 없음.
재현
```bash
cargo build --release
./target/release/rhwp export-svg samples/exam_math.hwp -o output/svg/exam_math/ -p 6
```
출력 `exam_math_007.svg`를 브라우저에서 열면 "수" 글리프가 `translate(290.9, 162.7)`에 배치됨. 정상 위치는 약 `translate(110, 162.7)` 부근.
근본 원인
위치: `src/renderer/layout/paragraph_layout.rs:1213-1226` (cross-run 우측/가운데 탭 감지 블록)
```rust
if has_tabs && run.text.ends_with('\t') {
if let Some(last_tab_pos) = run.text.rfind('\t') {
let text_before_tab = &run.text[..last_tab_pos];
let w_before = estimate_text_width(text_before_tab, &text_style);
let abs_before = text_style.line_x_offset + w_before;
let tw = if tab_width > 0.0 { tab_width } else { 48.0 };
let (tp, tt, _) = find_next_tab_stop( // ← inline_tabs 무시
abs_before, &tab_stops, tw, auto_tab_right, available_width,
);
if tt == 1 || tt == 2 {
pending_right_tab_render = Some((tp, tt)); // ← 잘못된 설정
}
}
}
```
마지막 `\t`의 종류를 판정할 때 `find_next_tab_stop`(TabDef 전용)만 사용하고, 본문 `tab_extended`(inline_tabs)를 참조하지 않음. 모든 TabDef stop이 `abs_before`보다 작으면 `auto_tab_right` 폴스루로 무조건 RIGHT 탭(type=1)으로 판정되어 다음 run이 우측 끝으로 역산 배치됨.
트레이스 증거 (paragraph 0.144)
```
IR: text="18.\t\t\t수열 이 모든 자연수 에 대하여"
tab_def: 12.0mm(L)/13.3mm(L)/18.0mm(L)/18.6mm(L) auto_tab_right=true
tab_extended: [132,,256,...] [671,,256,...] [79,_,256,...] # widths in HU, type=256(=LEFT)
Run "18.\t\t\t" (inline_tabs 경로): x=26.48 → 28.24 → 37.19 → 38.24 ✓ 정상
Cross-run 감지 (TabDef 경로): abs_before=37.19 → tab_stops 모두 < 37.69 → auto_tab_right → (tp=420.11, tt=1)
pending_right_tab_render = Some((420.11, 1))
Next run "수열 이 모든 자연수 " 배치:
next_w = 201.00
x = col_area.x + 420.11 - 201.00 = 71.80 + 219.11 = 290.91 ← SVG "수" 위치와 일치
```
수정 방향
`paragraph_layout.rs:1213-1226`의 cross-run 감지 블록을 다음과 같이 보정:
- `composed.tab_extended`가 해당 `\t` 인덱스를 커버하는 경우:
- `tab_extended[last_tab_idx][2]`를 탭 종류로 해석
- 0/256 등 → LEFT → `pending_right_tab_render` 설정하지 않음 (무처리)
- 1 → RIGHT, 2 → CENTER → 기존 경로 (단, 위치는 `ext[0]` 폭 누적으로 계산)
- `tab_extended`가 비었거나 부족한 경우에만 기존 `find_next_tab_stop` 폴백 유지.
회귀 방지
- `tests/` 또는 `output/re/`에 `samples/exam_math.hwp` page 7 item 18 첫 줄 스냅샷 픽스쳐 추가
- 시각 회귀 테스트: "수열" 글리프 x좌표가 col_area.x + 200 미만이어야 함
관련
증상
samples/exam_math.hwp페이지 7의 18번 "수열" 문항 첫 줄에서 "수열 {a_n}이 모든 자연수 n에 대하여" 텍스트가 좌측 열의 우측 끝으로 밀려나 렌더링됨.18. 수열 {a_n}이 모든 ...— "18."과 "수열" 사이는 짧은 공백18.·······················수열 {a_n}이 모든 ...— "수열"이 우측 끝 근처기타 문항(19, 20번)과 본 문항의 2·3째 줄은 영향 없음.
재현
```bash
cargo build --release
./target/release/rhwp export-svg samples/exam_math.hwp -o output/svg/exam_math/ -p 6
```
출력 `exam_math_007.svg`를 브라우저에서 열면 "수" 글리프가 `translate(290.9, 162.7)`에 배치됨. 정상 위치는 약 `translate(110, 162.7)` 부근.
근본 원인
위치: `src/renderer/layout/paragraph_layout.rs:1213-1226` (cross-run 우측/가운데 탭 감지 블록)
```rust
if has_tabs && run.text.ends_with('\t') {
if let Some(last_tab_pos) = run.text.rfind('\t') {
let text_before_tab = &run.text[..last_tab_pos];
let w_before = estimate_text_width(text_before_tab, &text_style);
let abs_before = text_style.line_x_offset + w_before;
let tw = if tab_width > 0.0 { tab_width } else { 48.0 };
let (tp, tt, _) = find_next_tab_stop( // ← inline_tabs 무시
abs_before, &tab_stops, tw, auto_tab_right, available_width,
);
if tt == 1 || tt == 2 {
pending_right_tab_render = Some((tp, tt)); // ← 잘못된 설정
}
}
}
```
마지막 `\t`의 종류를 판정할 때 `find_next_tab_stop`(TabDef 전용)만 사용하고, 본문 `tab_extended`(inline_tabs)를 참조하지 않음. 모든 TabDef stop이 `abs_before`보다 작으면 `auto_tab_right` 폴스루로 무조건 RIGHT 탭(type=1)으로 판정되어 다음 run이 우측 끝으로 역산 배치됨.
트레이스 증거 (paragraph 0.144)
```
IR: text="18.\t\t\t수열 이 모든 자연수 에 대하여"
tab_def: 12.0mm(L)/13.3mm(L)/18.0mm(L)/18.6mm(L) auto_tab_right=true
tab_extended: [132,,256,...] [671,,256,...] [79,_,256,...] # widths in HU, type=256(=LEFT)
Run "18.\t\t\t" (inline_tabs 경로): x=26.48 → 28.24 → 37.19 → 38.24 ✓ 정상
Cross-run 감지 (TabDef 경로): abs_before=37.19 → tab_stops 모두 < 37.69 → auto_tab_right → (tp=420.11, tt=1)
pending_right_tab_render = Some((420.11, 1))
Next run "수열 이 모든 자연수 " 배치:
next_w = 201.00
x = col_area.x + 420.11 - 201.00 = 71.80 + 219.11 = 290.91 ← SVG "수" 위치와 일치
```
수정 방향
`paragraph_layout.rs:1213-1226`의 cross-run 감지 블록을 다음과 같이 보정:
회귀 방지
관련