fix: line break 직전 inline TAC control 의 line 매핑 정정 — 시험지 page 2 cases formula off-by-one 해소 (closes #960)#963
fix: line break 직전 inline TAC control 의 line 매핑 정정 — 시험지 page 2 cases formula off-by-one 해소 (closes #960)#963jangster77 wants to merge 3 commits into
Conversation
… formula off-by-one 해소 (closes edwardkim#960) ## 본질 `samples/3-11월_실전_통합_2022.hwp` page 2 문14 (pi=117) 의 cases formula (multi-line equation g(x)={cases x...f(x)}) 가 line 0 영역 (y=329) 에 emit (예상 line 1 ~y=347, 한컴 PDF 정합) — header text 와 시각 overlap. ## Root cause `RHWP_DEBUG_PARA_TAC` 추적 결과: pi=117 text 에 FFFC (object replacement char) 없음. `find_control_text_positions` (model/paragraph.rs:817-838) 의 char_offsets gap 분석: - utf16 gap [60, 76] = 15 → 1 control at codepoint position 30 (= `\n`) compose_lines 의 line 1 chars range = [23, 30) — **position 30 (=\n) 제외**. `paragraph_layout.rs:1724-1727` filter: ```rust .filter(|(pos, _, _)| *pos >= run_char_pos && (*pos < run_char_end || (is_last_run && *pos == run_char_end))) ``` cases (pos=30) for line 1 (run_char_pos=23, run_char_end=30): - pos < 30 ❌, is_last_run ❌ (line 1 은 paragraph 의 last line 아님) - → cases 가 line 1 run_tacs 에서 누락 → shape_layout 의 default y (=329) 에 emit ## Fix `src/renderer/layout/paragraph_layout.rs:1719-1736`: ```rust // [Task edwardkim#960] has_line_break line 의 마지막 run 도 run_char_end 위치 의 TAC 포함. // HWP3 의 char_offsets gap 분석으로 매핑된 control 위치가 `\n` 문자에 떨어지면, // 그 line 의 chars range [start, end) 에서 end 가 `\n` 위치이므로 누락. let allow_end_tac = is_last_run || (comp_line.has_line_break && is_last_run_of_line(run_idx)); let run_tacs: Vec<(usize, f64, usize)> = tac_offsets_px.iter() .filter(|(pos, _, _)| *pos >= run_char_pos && (*pos < run_char_end || (allow_end_tac && *pos == run_char_end))) ... ``` ## 검증 - cargo test --release --lib: 1288 passed, 0 failed - 시험지 page 2 문14 cases formula y: 329 → 352 ✓ (line 1 정상) - TAC_LINE pi=117 line 1 run_tacs: [] → [(7, 177.85, 3)] ✓ - LAYOUT_OVERFLOW count: 41 → 41 (회귀 0) - exam_kor/math/eng, sample10~14, 시험지 4종: 시각 회귀 0 ## 영향 | 영역 | 영향 | |------|------| | has_line_break + end-position control | 정상화 (이전 누락 → line 내 inline emit) | | has_line_break 없는 line | 영향 없음 (조건 미진입) | | 일반 TAC control (line 안쪽) | 영향 없음 | ## 잔존 (별도 task) 본 task Stage 4 시각 검증 중 발견: - 문14 의 <보기> textbox (pi=118 InFrontOfText TAC 사각형 + 내부 글상자) 의 inline 수식 + ㄱㄴㄷ prefix scramble - Fix 적용 전/후 동일 (pre-existing, Fix 와 무관) - → 별도 issue edwardkim#962 등록 ## 관련 - 원 issue edwardkim#952 + PR edwardkim#956 (Issue 1 외곽선) - PR edwardkim#958 (Issue 2 sample16 page 18) - PR edwardkim#961 (Issue 3 시험지 page 1 문9 vertical) - 본 PR (Issue 4 cases formula off-by-one) - 신규 issue edwardkim#962 — page 2 보기 textbox 별도 task ## Note samples/pdfs (시험지 hwp/hwpx, pdf) 는 PR edwardkim#956 에서 추가되므로 본 PR 에 포함 안 함. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jangster77 — Issue #952 Issue 1: 페이지 외곽선 paper/body 잘못 분류 회귀. 회귀 source: 4bb1128 fix (#920) 의 paper_based = (attr & 0x01) == 0 비트 반전. 회귀 history: task877 (!= 0, sample16 정합/시험지 회귀) → #920 (== 0, 시험지 정합/sample16 회귀) → 본 PR (true, 모든 sample 한컴 정합). 진단: 5+ samples 한컴 viewer 실측 — attr 0/1, textBorder PAPER/CONTENT 양쪽 다 paper-based. bit 0 은 outline 위치 결정 비트 아님 (text wrap interaction 등 별 의미). 본질 (src/renderer/layout.rs +16/-2): - paper_based = true 강제 (#920 회귀 코드 대체) - RHWP_DEBUG_PAGE_BORDER 환경변수 진단 영구화 - 회귀 history 코멘트 영구 보존 (재회귀 방지) 추가: 시험지 fixture 8 (HWP/HWPX) + 한컴 2022 권위 PDF 4 (회귀 가드) + 문서 4. 본 PR 범위 외 (별도 task 분리, PR 본문 명시): - Issue 2: sample16 page 18 본문 밀림 (typeset multi-TAC cursor over-advance) - Issue 3: 시험지 문9 vertical 처짐 (HWP5 column) 자기 검증: cargo test --release --lib 1288 passed / clippy 통과 / 광범위 sweep 7 fixture / 169 페이지 / 169 same / 0 diff / WASM 4.4 MB 재빌드 시각 판정: 작업지시자 시각 검증 통과 (sample16 + 시험지 page border paper-based, 한컴 2022 PDF 권위) CI: ✅ Build & Test + CodeQL + Canvas visual diff 연속 5 PR 1번째 (#956 → #958 → #961 → #963 → #964, @jangster77 순차 처리)
@jangster77 — Issue #957 (Issue #952 영역 영역 Issue 2 분리 task): sample16 page 18 "나. 주요 과업내용" 후 본문 (pi=395~401) 다음 페이지 밀림. Root cause (RHWP_DEBUG_TAC_CURSOR 추적): pi=394 ci=1 picture 의 빈 caption (dir=Bottom width=0 paras=1 text="") 영역 영역 phantom +430.6px 누적 → pi=395 가 body 외 영역 emit → 다음 페이지 밀림. 본질 (src/renderer/layout.rs): caption_is_empty 가드 — caption 모든 paragraph 영역 영역 무의미 문자 (≤U+001F, U+FFFC) + controls.is_empty() 시 result_y advance skip. RHWP_DEBUG_TAC_CURSOR 진단 영구화 (PR #956 RHWP_DEBUG_PAGE_BORDER 패턴 정합). 영역 좁힘: 빈 caption + Bottom 한정 — non-empty/None/Top 영향 없음. 본 환경 충돌 수동 해결: - mydocs/orders/20260517.md — 본 환경 PR #956 처리 섹션 + PR #958 Task #957 작업 일지 양측 보존 통합 - src/renderer/layout.rs — PR #956 (paper_based :770) + PR #958 (caption :3491) 다른 영역 auto-merge (양립 확인) 자기 검증: cargo test --release --lib 1288 passed / clippy 통과 / 광범위 sweep 7 fixture / 169 페이지 / 169 same / 0 diff / WASM 4.4 MB 재빌드 시각 판정: 작업지시자 시각 검증 통과 (sample16 page 18 본문 같은 페이지, sample14/10/11/13 + PR #956 page border 회귀 부재) CI: ✅ Build & Test + CodeQL + Canvas visual diff 연속 5 PR 2번째 (#956 ✅ → #958 → #961 → #963 → #964)
… column 외부 emit advance skip @jangster77 — Issue #959 (Issue #952 영역 영역 Issue 3 분리 task): 시험지 (3-11월) page 1 우측 단 문9 ~250px 처짐. Root cause (RHWP_DEBUG_TAC_CURSOR 추적): Shape pi=69 ci=0 picture (horz_rel_to=Column, h_offset=79.5mm, Center) 영역 영역 pic_emit_x=767 > col_area right=759.7 → picture 좌측 edge column 외부, 한컴 PDF 우측 단 미표시. 그럼에도 cursor 274px advance → 문9 처짐. 본질 (src/renderer/layout.rs:3537): saved_y_offset — horz_rel_to=Column picture pic_emit_x 가 col_area 우측 초과 시 result_y = saved_y_offset (advance skip, :3566). 영역 좁힘: horz_rel_to=Column + col 외부 한정 — col 내부/Paper/Page/Para/TAC 영향 없음. 본 환경 충돌 수동 해결: - mydocs/orders/20260517.md — 본 환경 PR #956/#958 처리 표 + PR #961 Task #959/#960 작업 일지 양측 보존 통합 - src/renderer/layout.rs — auto-merge: PR #956 paper_based=true :770 + PR #958 caption_is_empty :3491 + PR #961 saved_y_offset :3537 3 정정 양립 - 시험지 fixture 8 + PDF 4 — PR #956 머지 영역 영역 동일 content (devel 보존) 자기 검증: cargo test --release --lib 1288 passed / clippy 통과 / 광범위 sweep 7 fixture / 169 페이지 / 169 same / 0 diff / WASM 4.4 MB 재빌드 시각 판정: 작업지시자 시각 검증 통과 (시험지 page 1 문9 y=805 한컴 PDF 정합, 시험지 4종 + exam_kor p18 + PR #956/#958 회귀 부재) CI: ✅ Build & Test + CodeQL + Canvas visual diff Issue #952 종결 — Issue 1 (#956) + Issue 2 (#958) + Issue 3 (#961) 모두 해결. 잔존: Issue #960 (page 2 multi-line equation off-by-one, pre-existing, 별도) 연속 5 PR 3번째 (#956 ✅ → #958 ✅ → #961 → #963 → #964)
@jangster77 — Issue #960 (Issue #952 영역 영역 Issue 4, PR #961 page 2 검증 중 발견): 시험지 (3-11월) page 2 문14 (pi=117) cases formula 가 line 0 (y=329) 에 emit → header text 와 overlap. Root cause (RHWP_DEBUG_PARA_TAC 추적): char_offsets gap 분석 영역 영역 cases (ci=3) → position 30 (= \n 위치). compose_lines line 1 chars [23,30) 에서 position 30 제외 → is_last_run && pos == end 만 허용 → line 1 (last line 아님) 누락 → line 0 (header) emit → overlap. 본질 (src/renderer/layout/paragraph_layout.rs:1730): allow_end_tac = is_last_run || (comp_line.has_line_break && is_last_run_of_line(run_idx)) — has_line_break line 의 마지막 run 도 end position TAC 포함. RHWP_DEBUG_PARA_TAC 진단 영구화. 영역 좁힘: has_line_break + end-position 한정 — has_line_break 없는 line / 일반 TAC (line 안쪽) 영향 없음. 본 환경 충돌 수동 해결: mydocs/orders/20260517.md (--ours 본 환경 PR 처리 표 보존 + Task #960/#962 작업 일지 갱신). paragraph_layout.rs auto-merge (devel 변경 부재, PR #956/#958/#961 은 layout.rs — 다른 파일, 4 정정 양립). 자기 검증: cargo test --release --lib 1288 passed / clippy 통과 / 광범위 sweep 7 fixture / 169 페이지 / 168 same / 1 diff (exam_math_017.svg). exam_math page 18 inline equation line 매핑 변화 — 작업지시자 한컴 2020 에디터 직접 확인 영역 영역 **의도된 정정 확정** (이전 잘못된 line emit → 정정 후 올바른 line, 시험지 page 2 cases 와 동일 본질). WASM 4.4 MB 재빌드. 시각 판정: 작업지시자 시각 검증 통과 (시험지 page 2 cases y=352 한컴 PDF 정합 + exam_math page 18 의도 확정 + 회귀 부재) CI: Build & Test pending (devel merge commit 재트리거) — 본 환경 자기 검증 보완 잔존: Issue #962 (page 2 보기 textbox scramble, pre-existing, 별도 task) 연속 5 PR 4번째 (#956 ✅ → #958 ✅ → #961 ✅ → #963 → #964)
|
@jangster77 머지 완료 (commit RHWP_DEBUG_PARA_TAC + char_offsets gap 분석 영역 영역 cases (ci=3) position 30 ( 본 환경 자기 검증:
exam_math page 18 sweep diff — inline equation line 매핑 변화 발견. PR 본문 영역 영역 "exam_math 회귀 0" 명시였으나 본 환경 영역 영역 diff 1건 검출. 작업지시자 한컴 2020 에디터 직접 확인 영역 영역 의도된 정정 확정 — 이전 equation 영역 영역 잘못된 line (y=343.8) 영역 영역 emit → 정정 후 올바른 line (y=301.7), 시험지 page 2 cases 와 동일 본질. 회귀 아님, 추가 정합 효과. PR #956/#958/#961 (layout.rs) + #963 (layout/paragraph_layout.rs) 4 정정 양립 (다른 파일). WASM 4.4 MB 재빌드. 작업지시자 시각 판정 통과. 잔존: Issue #962 (page 2 보기 textbox scramble, pre-existing, 별도 task). 수고하셨습니다. 연속 PR #964 이어서 진행하겠습니다. |
- mydocs/pr/archives/pr_963_review.md (TAC line 매핑 off-by-one 분석) - mydocs/pr/archives/pr_963_report.md (옵션 A + exam_math p18 sweep diff 의도 확정) - mydocs/orders/20260517.md PR #963 행 추가 핵심: - pi=117 cases formula position 30 (\n) off-by-one (RHWP_DEBUG_PARA_TAC) - allow_end_tac (has_line_break line 마지막 run end-position TAC 포함) - PR #956/#958/#961 (layout.rs) + #963 (paragraph_layout.rs) 4 정정 양립 - sweep 168/169 same + exam_math p18 diff 1건 → 작업지시자 한컴 2020 직접 확인 의도 확정 - Issue #960 close, Issue #962 잔존 (별도 task, 연속 PR #964 후속)
…ox content scramble 해소 (closes #962) `samples/3-11월_실전_통합_2022.hwp` page 2 문14 <보기> textbox (pi=118 InFrontOfText TAC 사각형 + 내부 글상자) 의 inline 수식이 각각 2번 emit → ㄱㄴㄷ prefix + 본문 + 수식 시각 overlap. `RHWP_DEBUG_PARA_TAC` + SVG 분석: - 보기 textbox 영역 (y 440-540, x 400-760) 의 equation transforms: **12 개** (예상 6 × 2 duplicates) - Set 1 (gap 위치, 정상): paragraph_layout 의 inline TAC 처리 (paragraph_layout.rs:2078+) - Set 2 (textbox 좌측 edge x=406): shape_layout 두번째 loop 의 Equation branch (shape_layout.rs:1609) 원래 두번째 loop 의 의도: paragraph_layout 미지원 이전의 legacy fallback. 현재 paragraph_layout 가 textbox 내부 inline TAC 를 정상 처리하므로 중복 emit 발생. `src/renderer/layout/shape_layout.rs:1609-1675`: ```rust Control::Equation(eq) => { let eq_w = hwpunit_to_px(eq.common.width as i32, self.dpi); let eq_h = hwpunit_to_px(eq.common.height as i32, self.dpi); // [Task #962] 글상자 내부 paragraph 의 inline equation 은 paragraph_layout 가 // 정확한 gap 위치 (text 사이) 에 emit. 본 두번째 loop 는 legacy fallback. let equiv_cell_ctx = CellContext { parent_para_index: para_index, path: { /* parent_cell_path + textbox entry */ }, }; if tree.get_inline_shape_position( section_index, pi, ctrl_idx_in_para, Some(&equiv_cell_ctx) ).is_some() { // paragraph_layout 가 이미 emit — inline_x 만 advance inline_x += eq_w; } else { // legacy fallback (기존 emit 분기 유지) ... } } ``` - cargo test --release --lib: 1288 passed, 0 failed - 시험지 page 2 보기 textbox equations: 12 → 6 ✓ (duplicates 제거) - 시각: ㄱ. h(1)=3 / ㄴ. 함수 h(x)는... / ㄷ. 함수 g(x)가... ✓ 한컴 PDF 정합 - LAYOUT_OVERFLOW count: 325 → 325 (회귀 0) - exam_kor/math/eng, sample14, 시험지 4종: 시각 회귀 0 | 영역 | 영향 | |------|------| | textbox 내부 inline Equation (paragraph_layout 등록 case) | duplicate 제거 (회귀 fix) | | textbox 내부 inline Equation (legacy fallback) | 영향 없음 (else 분기 유지) | | textbox 내부 Shape/Picture/Table | 본 fix 미대상 | | textbox 외부 standalone equation | 영향 없음 | - 원 issue #952 + PR #956 (Issue 1 외곽선) - PR #958 (Issue 2 sample16 page 18) - PR #961 (Issue 3 시험지 page 1 문9 vertical) - PR #963 (Issue 4 cases formula off-by-one) - 본 PR (Issue 5 보기 textbox duplicate equation) 원 #952 의 5 issue 모두 해결 → close 가능. samples/pdfs (시험지) 는 이전 PR (#956/#961) 에서 추가되므로 본 PR 에 포함 안 함 (중복 방지). layout.rs 변경은 PR #958/#961 영역 — 본 PR 에 포함 안 함. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jangster77 — Issue #962 (Issue #952 영역 영역 Issue 5, PR #963 page 2 검증 중 발견): 시험지 (3-11월) page 2 문14 <보기> textbox inline 수식 각각 2번 emit → scramble. Root cause (SVG 분석): 보기 textbox 영역 영역 equation 12개 (6×2 duplicates): - Set 1 (정상, gap 위치): paragraph_layout inline TAC (paragraph_layout.rs:2078+) - Set 2 (duplicate, textbox 좌측 edge): shape_layout 두번째 loop legacy fallback paragraph_layout 가 현재 textbox 내부 inline TAC 정상 처리 → legacy fallback 중복. 본질 (src/renderer/layout/shape_layout.rs:1620): Equation branch emit 전 get_inline_shape_position (equiv_cell_ctx — parent_cell_path + textbox entry) 확인 → paragraph_layout 등록 시 inline_x += eq_w 만 (duplicate 차단), 미등록 시 legacy fallback 유지. 영역 좁힘: paragraph_layout 등록 case 한정 — legacy fallback / Shape/Picture/Table / standalone 영향 없음. 본 환경 충돌 수동 해결: mydocs/orders/20260517.md (--ours 본 환경 PR 처리 표 보존 + Task #962 작업 일지 갱신). shape_layout.rs auto-merge (devel 변경 부재). PR #956/#958/#961 (layout.rs) + #963 (paragraph_layout.rs) + 본 PR (shape_layout.rs) — **5 정정 모두 다른 파일, 양립 확인**. devel merge commit (93dfe0a/3d1cdf31) cherry-pick 제외 — 본질 682875f 만. 자기 검증: cargo test --release --lib 1288 passed / clippy 통과 / 광범위 sweep 7 fixture / 169 페이지 / 169 same / 0 diff / WASM 4.4 MB 재빌드 시각 판정: 작업지시자 시각 검증 통과 (시험지 page 2 보기 textbox 12→6, ㄱ/ㄴ/ㄷ 한컴 PDF 정합 + 회귀 부재) CI: ✅ Build & Test + CodeQL + Canvas visual diff 원 Issue #952 의 5 분리 결함 완결: #952 Issue 1 (#956) + Issue 2 (#958) + Issue 3 (#961) + #960 (#963) + #962 (본 PR) 연속 5 PR 완결 (@jangster77: #956 → #958 → #961 → #963 → #964)
- mydocs/pr/archives/pr_964_review.md (textbox inline equation duplicate 분석) - mydocs/pr/archives/pr_964_report.md (옵션 A + Issue #952 5 분리 결함 완결) - mydocs/orders/20260517.md PR #964 행 + 연속 5 PR 총평 핵심: - 보기 textbox equation 12개 (Set 1 paragraph_layout + Set 2 shape_layout legacy duplicate) - get_inline_shape_position (equiv_cell_ctx) 확인 영역 영역 duplicate 차단 - PR #956~#964 5 정정 다른 파일 양립 (layout.rs×3 + paragraph_layout.rs + shape_layout.rs) - sweep 169/169 same + 작업지시자 시각 판정 통과 - 원 Issue #952 의 5 분리 결함 완결 (#956/#958/#961/#963/#964) - @jangster77 연속 5 PR 완결
요약
samples/3-11월_실전_통합_2022.hwppage 2 문14 (pi=117) 의 cases formula 가 line 0 영역 (y=329) 에 emit (예상 line 1 ~y=347) → header text 와 시각 overlap.Root cause
`RHWP_DEBUG_PARA_TAC` 추적:
\n)\n) 제외pos < end또는is_last_run && pos == end만 허용 → line 1 (last line 아님) 누락Fix
`src/renderer/layout/paragraph_layout.rs:1719-1736`:
has_line_break line 의 마지막 run 도 end position 의 TAC 포함.
검증
영향
잔존 (별도 task)
본 PR 의 page 2 시각 검증 중 발견 — 보기 textbox content scramble (pi=118 InFrontOfText TAC 사각형 + 내부 글상자) → 별도 issue #962 (Fix 적용 전/후 동일, pre-existing).
관련
Note
samples/pdfs (시험지) 는 PR #956 / #961 에서 추가되므로 본 PR 에 포함 안 함 (중복 방지).
Test plan
🤖 Generated with Claude Code