Skip to content

Task #290: cross-run 탭 감지가 inline_tabs 무시하여 좌측 탭이 우측 정렬로 오판정#292

Merged
edwardkim merged 6 commits into
edwardkim:develfrom
planet6897:local/task290
Apr 24, 2026
Merged

Task #290: cross-run 탭 감지가 inline_tabs 무시하여 좌측 탭이 우측 정렬로 오판정#292
edwardkim merged 6 commits into
edwardkim:develfrom
planet6897:local/task290

Conversation

@planet6897

Copy link
Copy Markdown
Contributor

요약

  • samples/exam_math.hwp 페이지 7 의 18번 "수열" 문항 첫 줄이 좌측 단의 우측 끝으로 밀려 렌더되던 버그를 해결 (closes cross-run 탭 감지가 inline_tabs를 무시하여 좌측 탭이 우측 정렬로 오판정 (exam_math.hwp p.7 #18) #290).
  • src/renderer/layout/paragraph_layout.rscross-run 우측/가운데 탭 감지 블록 (est 측 :854-868 + render 측 :1213-1226) 이 마지막 \t 의 종류를 판정할 때 find_next_tab_stop (TabDef 전용) 만 호출하여 본문 tab_extended (inline_tabs) 를 무시했음. LEFT inline 탭이 auto_tab_right 폴스루로 RIGHT 오판되어 다음 run 이 우측 끝으로 역산 배치됨.
  • 신규 헬퍼 resolve_last_tab_pending 으로 inline_tabs 우선 참조 + TabDef 폴백 경로 통합. est/render 양쪽 루프에 inline_tab_cursor_* 도입.
  • 결과: "수" 글리프 x=290.9 → x=109.8 (PDF 일치). 회귀 4 문서 184 페이지 중 1 페이지만 변경 (의도 100%).

배경과 원인

작업지시자 제보: samples/exam_math.hwp p.7 #18 문항 첫 줄이 PDF 와 다르게 보임.

PDF (정답): 18. 수열 {a_n}이 모든 자연수 n에 대하여
SVG (버그): 18. ··························· 수열 {a_n}이 모든 자연수 n에 대하여

IR 관찰 (paragraph 0.144)

text     : "18.\t\t\t수열 이 모든 자연수 에 대하여"
tab_def  : [12.0mm(L), 13.3mm(L), 18.0mm(L), 18.6mm(L)]  auto_tab_right=true
inline   : [132,_,256,...] [671,_,256,...] [79,_,256,...]  # ext[2]=0x0100 → LEFT

트레이스로 확정한 메커니즘 (임시 RHWP_TRACE290)

  1. Run "18.\t\t\t" 의 3 개 \t 는 inline 경로에서 LEFT 로 x=38.24 까지 진행 (정상).
  2. 그러나 cross-run 감지는 TabDef 만 봄 → abs_before=37.19 > 모든 stops → auto_tab_right 폴스루 → type=1 (RIGHT) 반환.
  3. pending_right_tab_render = Some((420.11, 1)) 설정.
  4. 다음 run "수열 이 모든 자연수 " 배치 시 x = col_area.x + 420.11 - next_w(201.00) = 290.91 로 역산 → 우측 끝 근처 배치.

ext[2] 포맷 실증 (Stage 1)

RIGHT 샘플 (samples/hwp-3.0-HWPML.hwp 저작권\t1) 확보 + 트레이스 비교:

케이스 ext[2] 16진 high low
exam_math #18 (LEFT × 3) 256 0x0100 1 0
hwp-3.0-HWPML 저작권 (RIGHT, fill=3) 515 0x0203 2 3

ext[2] 는 high/low byte 합성 값. high=탭 종류 enum+1 (1=LEFT, 2=RIGHT, 3=CENTER, 4=DECIMAL), low=fill_type.

수정 내용

1. 신규 헬퍼 resolve_last_tab_pending

pub(crate) fn resolve_last_tab_pending(
    run_text: &str,
    last_inline_idx: usize,
    tab_extended: &[[u16; 7]],
    text_style: &TextStyle,
    tab_stops: &[TabStop],
    tab_width: f64,
    auto_tab_right: bool,
    available_width: f64,
) -> Option<(f64, u8)> {
    // 1) inline_tabs 가 마지막 \t 를 커버: ext[2] 고바이트로 종류 판정
    if last_inline_idx < tab_extended.len() {
        let inline_type = ((tab_extended[last_inline_idx][2] >> 8) & 0xFF) as u8;
        match inline_type {
            0 | 1 => return None,  // LEFT → pending 없음 (본 수정의 핵심)
            2 | 3 => {}            // RIGHT/CENTER → TabDef 경로로 폴스루
            _ => return None,      // 미지 값 (4=DECIMAL 등) → 보수적 LEFT
        }
    }
    // 2) inline 이 LEFT 아님 or inline 없음 → 기존 find_next_tab_stop 경로
    /* ... */
}

2. cross-run 블록 2 곳 교체 + inline_tab_cursor_*

  • est 측 루프 (:840): inline_tab_cursor_est: usize = 0 도입, 기존 블록을 헬퍼 호출로 교체.
  • render 측 루프 (:1198): inline_tab_cursor_render: usize = 0 도입, 동일 교체.
  • 루프 말미 + char_overlap continue 직전에 cursor += run.text.chars().filter(|c| *c == '\t').count() 증가.
  • composed.tab_extended 는 parser 에서 0x0009 (TAB) 마다 1 개씩 push 되므로 \t 카운트와 정확히 일치.

3. 테스트 신규

  • 단위 5 건 (src/renderer/layout/tests.rs): LEFT→None / RIGHT→Some / CENTER→Some / inline 없음 폴백 2 건.
  • 통합 1 건 (tests/tab_cross_run.rs): exam_math.hwp p.7 렌더 후 item 18 "수" glyph x < 200 검증.

변경 전/후 (SVG transform)

item 18 첫 줄의 14 글자 모두 일관되게 -181.11 px 좌측 이동:

글리프 변경 전 x 변경 후 x
290.91 109.80
304.86 123.75
354.73 173.63
... ... ... (일관 -181.11)

검증

항목 결과
cargo test --lib task290 (신규 5) 5/5 pass
cargo test --test tab_cross_run (신규 1) 1/1 pass
cargo test --test svg_snapshot 3/3 pass
cargo test --lib 전체 955 pass (선존재 14 fail 은 cfb_writer/wasm_api, 무관)
cargo clippy --lib -- -D warnings clean

회귀 검증 (git worktree baseline diff)

문서 변경 / 전체
exam_math.hwp 1 / 20 (p.7 item 18 의도된 수정)
biz_plan.hwp 0 / 6
exam_eng.hwp 0 / 11
exam_kor.hwp 0 / 25
hwp-3.0-HWPML.hwp 0 / 122 (RIGHT inline tab 저작권\t1 회귀 없음)
합계 1 / 184 (의도 외 변화 0)

시각 비교 (3 면 PNG)

mydocs/working/task_m100_290_stage3/p7_{before,after,pdf}.png — AFTER = PDF 일치.

범위 외 후속 과제

교훈

  1. 수식 레이아웃 보정: 한컴 기준값 측정 및 상수 조정 #142 의 "같은 데이터를 다른 경로로 계산하는 코드는 반드시 동기화" 교훈이 이번엔 estimate_text_width/compute_char_positions 가 아니라 run 내부 탭 처리 vs cross-run 탭 감지 의 불일치로 재발. 런
    내부는 inline_tabs 를 봤지만 cross-run 감지는 TabDef 만 봤음. 헬퍼 중앙화 (resolve_last_tab_pending) 로 경계 통일.
  2. 트레이스 기반 원인 확정의 위력 — 임시 RHWP_TRACE290 로 양쪽 호출의 입력/출력을 동시 관측 → 추측 없이 pending_right_tab = Some((420.11, 1))x_after=290.91 로 이어지는 전 경로를 숫자로 연결.
  3. git worktree 활용 baseline diff — fix 전 커밋의 독립 빌드로 184 페이지 byte-level 회귀를 자동 검증. "변경 / 전체 = 1 / 184" 같은 객관적 지표 제공.
  4. 범위 의식적 제어 — inline_tabs RIGHT/CENTER 렌더 버그를 발견했지만 본 타스크 범위에 포함하지 않고 후속 이슈로 분리. 범위가 커지면 회귀 위험도 증가.

산출물

코드

  • src/renderer/layout/paragraph_layout.rs — 헬퍼 추가 + cross-run 블록 2 곳 교체 + cursor 2 개 도입 (+86 / -24 줄)
  • src/renderer/layout/tests.rs — 단위 테스트 5 건 (+83 줄)
  • tests/tab_cross_run.rs — 통합 테스트 1 건 (신규)

문서

커밋

1e9d42a Task #290 단계4: 최종 보고서 + 오늘할일/트러블슈팅 갱신 (closes #290)
0d2b747 Task #290 단계3: 통합 테스트 + 184페이지 회귀 + before/after/PDF 3면 시각 비교
3c8bc4f Task #290 단계2: resolve_last_tab_pending 헬퍼 + cross-run 블록 교체 + 단위 테스트 5건
7d3bbba Task #290 단계1: 원인 트레이스 + ext[2] 매핑 실증 + 계획서

planet6897 and others added 6 commits April 24, 2026 16:10
- 수행계획서 `task_m100_290.md`, 구현계획서 `task_m100_290_impl.md`
- Stage 1 완료 보고서 `task_m100_290_stage1.md`
- exam_math.hwp p.7 edwardkim#18 "수열" 문제: cross-run 탭 감지가 `composed.tab_extended` 를 무시하여 LEFT inline 탭이 `auto_tab_right` 폴스루로 RIGHT 탭으로 오판, 다음 run 이 우측 끝으로 역산 배치됨
- 근본 위치: `src/renderer/layout/paragraph_layout.rs:1213-1226` (render 측) + `:854-868` (est 측)
- `ext[2]` 포맷 실증: hi byte = 탭 종류 enum+1 (1=LEFT, 2=RIGHT, 3=CENTER, 4=DECIMAL), lo byte = fill
- inline_tab_cursor 도입 위치 결정, 옵션 A (TabDef 기반 위치) 유지
- 오늘할일 `20260424.md` 에 edwardkim#290 신규 등록 항목 추가
… + 단위 테스트 5건

- 신규 헬퍼 `resolve_last_tab_pending` (`paragraph_layout.rs`):
  inline_tabs 가 `last_inline_idx` 를 커버하면 `ext[2] >> 8` 고바이트로 탭 종류 판정.
  0/1(LEFT) → None (본 수정 핵심), 2/3(RIGHT/CENTER) → TabDef 기반 기존 경로.
- est 측 (`:840`) + render 측 (`:1198`) 의 cross-run 탭 감지 블록 2곳을 헬퍼 호출로 교체.
  각 루프에 `inline_tab_cursor_*` 변수 도입, char_overlap continue 및 루프 말미에서
  `run.text.chars().filter(|c| *c == '\t').count()` 만큼 증가.
- 단위 테스트 5건 (`task290_*`): LEFT→None, RIGHT→Some, CENTER→Some, 폴백 2건.
  모두 pass. 전체 cargo test --lib 955 pass (기존 14 fail 은 선존재 cfb_writer).
- 스폿 검증: exam_math p.7 item 18 "수" 위치 290.9 → 109.8 (정상),
  hwp-3.0-HWPML 저작권 RIGHT 케이스 회귀 없음 ("1" 우측 정렬 유지).
- 통합 테스트 `tests/tab_cross_run.rs` 신규: exam_math.hwp p.7 렌더 후
  item 18 "수" 글리프 x < 200 px 검증. 1/1 pass.
- git worktree 로 baseline 생성 → 주요 샘플 전체 페이지 SVG diff:
  * exam_math.hwp: 1/20 변경 (p.7 only, 의도된 item 18 수정)
  * biz_plan.hwp: 0/6, exam_eng.hwp: 0/11, exam_kor.hwp: 0/25
  * hwp-3.0-HWPML.hwp: 0/122 (RIGHT inline tab 회귀 없음)
  * 총 184페이지 중 1페이지만 변경 (의도 100%)
- p.7 diff 14줄 모두 item 18 첫 줄 14글자 -181.11px 좌측 이동
- 시각 비교 PNG 3면 (before/after/pdf) 저장:
  mydocs/working/task_m100_290_stage3/p7_{before,after,pdf}.png
  → AFTER 가 PDF 와 동일하게 "18. 수열 {a_n}이 모든 ..." 좌측 정렬
- 최종 결과 보고서 `task_m100_290_report.md`: 배경/원인/수정/검증/후속과제/교훈 5축 정리.
- 오늘할일 `20260424.md`: "4. Task edwardkim#290" 섹션 신설 + 이슈 활동 표 종료로 이동.
- 트러블슈팅 `tab_tac_overlap_142_159.md`: "후속 사건: edwardkim#290" 섹션 추가.
  edwardkim#142 교훈 ("같은 데이터 다른 경로 계산 시 동기화") 의 범위가
  estimate_text_width vs compute_char_positions 에서
  "run 내부 탭 처리 vs cross-run 탭 감지" 로 확장됨을 기록.
- Stage 4 보고서 `task_m100_290_stage4.md`: 4단계 종료 + 검증 지표 총괄.
# Conflicts:
#	mydocs/orders/20260424.md

@edwardkim edwardkim left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. 🎉

@planet6897 님, 훌륭한 조사·수정입니다.

평가 포인트

  • 임시 트레이스 RHWP_TRACE290 — 'pending_right_tab=Some((420.11,1)) → x_after=290.91' 전 경로를 숫자로 연결
  • ext[2] 포맷 실증 — RIGHT 샘플 (hwp-3.0-HWPML 저작권\t1) 확보로 high/low 바이트 구조 검증
  • #142 교훈 재적용 — 'resolve_last_tab_pending' 헬퍼 중앙화. 트러블슈팅 문서 확장 기록
  • git worktree baseline diff — 184 페이지 byte-level 자동 검증 (1/184 의도 100%)
  • 범위 의식적 제어 — inline_tabs RIGHT/CENTER 렌더 버그 발견해도 후속 이슈로 분리

메인테이너 검증

항목 결과
cargo test --lib (merge 시뮬레이션) ✅ 988 / 0 / 1 ignored
cargo test --test tab_cross_run ✅ 1 / 0 (신규)
cargo test --test svg_snapshot ✅ 6 / 0
cargo clippy / wasm32 ✅ clean
CI (원본) ✅ SUCCESS

처리 절차

  • orders 문서 충돌 (Task #290 vs #288 섹션) 메인테이너 직접 해결
  • planet6897/local/task290 에 push (bc6c46d..206e265)
  • 재승인 후 admin merge

후속 관찰 (별도 이슈 등록 예정)

브라우저 WASM Canvas 경로(WasmTextMeasurer::estimate_text_width / compute_char_positions)에는 본 PR 수정이 도달하지 않음. SVG 경로는 paragraph_layout.rs 에서 고쳤지만 Canvas 경로는 text_measurement.rs 의 find_next_tab_stop 만 사용하여 동일한 'LEFT inline tab → auto_tab_right 폴스루 오판' 증상이 브라우저에서 재현. 별도 이슈로 등록해 핀셋 처리 예정.

본 PR 의 SVG 경로 수정은 의도대로 완결적 — admin merge 진행합니다.

@edwardkim edwardkim merged commit 085beb0 into edwardkim:devel Apr 24, 2026
6 checks passed
edwardkim added a commit that referenced this pull request Apr 24, 2026
…#290)

- 작성자: @planet6897 (Task #290, PR #292)
- Merge commit: 085beb0 (admin merge, orders 충돌 직접 해결)
- 이슈 #290 CLOSED

처리 절차:
- PR 브랜치에 origin/devel 머지 → orders 섹션 재배치 (#290 "## 6", #288 "## 7")
- planet6897/local/task290 에 push (maintainerCanModify 허용)
- 재승인 + admin merge

검증:
- cargo test --lib: 988 passed / 0 failed / 1 ignored (+5 신규 task290)
- cargo test --test tab_cross_run: 1 passed (신규)
- svg_snapshot: 6 passed, clippy + wasm32 check: clean
- CI (원본): 전부 SUCCESS
- CLI SVG: p.7 #18 "수" glyph x=109.80 정상

후속 이슈 등록:
- #296 (WASM Canvas 경로 inline_tabs 무시) — 본 PR의 Canvas 버전, 메인테이너 핀셋 처리 예정

별도 추적 중:
- #291 (KTX.hwp 2단 TAC 표 회귀)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
edwardkim added a commit that referenced this pull request Apr 24, 2026
PR #292 (#290) 머지 후 브라우저 검증에서 exam_math.hwp p.7 #18 "수열" 문항이
여전히 우측으로 밀림 확인. 원인: WasmTextMeasurer 에 inline_tabs 분기 부재로
TabDef 만 참조 → auto_tab_right 폴스루 → RIGHT 오판.

변경:
- src/renderer/layout/text_measurement.rs (+69 -2):
  - 헬퍼 inline_tab_type(ext) = (ext[2] >> 8) & 0xFF 신규 (pub(super))
  - WasmTextMeasurer::estimate_text_width 에 inline_tabs 분기 신규 (+23)
  - WasmTextMeasurer::compute_char_positions 에 동일 분기 신규 (+27)
  - match arm: 2 => RIGHT, 3 => CENTER, _ => LEFT/DECIMAL (PR #292 실증 포맷)
  - 네이티브 EmbeddedTextMeasurer 는 건드리지 않음 — 기존 golden 2건
    (issue-147, issue-267) 이 우연한 LEFT 폴백에 의존 중이라 범위 축소.
    한컴 PDF 대조 후 별도 이슈로 처리 예정.
- src/renderer/layout/tests.rs (+32): task296_inline_tab_type_{left,right,center,decimal} 4건

범위 축소 결정:
Stage 2 중간에 네이티브 측정기도 수정했을 때 svg_snapshot 2건 FAIL 발생.
기존 golden 이 "우연한 LEFT 폴백" 동작에 의존 중임을 확인 → 무리하게 golden
을 갱신하는 대신 WASM 만 수정하고 네이티브 측은 별도 이슈로 분리.
inline_tab_type 을 pub(super) 로 공개해두어 후속 이슈에서 재사용 가능.

검증:
- cargo test --lib: 992 passed / 0 failed / 1 ignored (988 → 992, +4 신규)
- cargo test --test svg_snapshot: 6 passed (기존 golden 유지)
- cargo test --test tab_cross_run: 1 passed (#290 회귀 없음)
- cargo clippy / wasm32 check: clean
- WASM Docker 빌드 + rhwp-studio 브라우저 시각 검증: 작업지시자 판정 성공

산출물:
- mydocs/plans/task_m100_296{,_impl}.md
- mydocs/working/task_m100_296_stage{1,2,3,4}.md
- mydocs/report/task_m100_296_report.md
- mydocs/troubleshootings/tab_tac_overlap_142_159.md ("#296 섹션" 추가)
- mydocs/orders/20260424.md (Task #296 섹션 + 종료 리스트 갱신)

후속 이슈 후보: 네이티브 EmbeddedTextMeasurer 의 tab_type = ext[2] 버그
(한컴 PDF 대조로 올바른 동작 확정 후 inline_tab_type 재사용하여 수정)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
edwardkim added a commit that referenced this pull request Apr 24, 2026
 #297)

- 작성자: @planet6897 (Task #297, PR #300, 오늘 6번째 기여)
- Merge commit: 0e3fb02 (admin merge, orders 3구간 충돌 직접 해결)
- 이슈 #297 CLOSED

처리 절차:
- PR 브랜치에 origin/devel 머지 → orders 섹션 3구간 해결 (#295 "## 7", #296 "## 8", #297 "## 9")
- planet6897/task297 에 push
- 재승인 + admin merge

변경 (1파일):
- src/renderer/layout/table_layout.rs +5 -2:
  - VertRelTo::Page => (col_area.y, col_area.height)  [쪽 본문 영역]
  - VertRelTo::Paper => (0, page_h_approx)  [용지 전체, 유지]
  - HWP 스펙 Page=쪽 본문, Paper=용지 전체 반영

성과:
- pi=22 "* 확인 사항" 박스 y: 1371.5 → 1224.07 (PDF 1226.5 ±2 일치)
- 145 샘플 중 본문 Page 표 13건 + 바탕쪽 5건 회귀 스캔 완료 (의도 범위 외 무회귀)

검증:
- cargo test --lib: 992 passed
- svg_snapshot: 6 passed (golden 유지)
- 실제 SVG y 좌표 확인: 1224.07px (PDF 일치)

#295#297 연결 모범 사례: PR #298 리뷰 중 사전 존재 버그로 분리 → 1시간 만에 PR #300 해결.
초기 가설(바탕쪽 Paper) 폐기 → pdftotext 실측으로 근본 원인(enum 미구분) 발견 → 1줄 수정.

===== 오늘 9번째 PR 머지 =====
#284 #285 #266 #273 #277 #278 #289 #292 #298 #300
+ 메인테이너 핀셋 #296

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@planet6897 planet6897 deleted the local/task290 branch April 30, 2026 00:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants