Fix #412: 다단 우측 단 단행 문단 줄간격 누락 (vpos 보정 anchor 도입)#424
Conversation
Release v0.7.6: 외부 기여자 다수 + 조판 정밀화
edwardkim#354 (CodeQL Alert), edwardkim#359 (fit drift), edwardkim#361 (page_num + PartialTable fit), edwardkim#362 (PartialTable + Square wrap 8항목 누적) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- row_block_start/end 필드 + compute_row_blocks 헬퍼 - row_block_for / snap_to_block_boundary 메서드 - 신규 단위 테스트 7개 (rowspan 단일/겹침/비인접 + 폴백 + 스냅) 회귀 검증용 샘플 hwpx/pdf 동반 커밋.
- pagination/engine.rs::split_table_rows: pre-loop first_block_h, snap_to_block_boundary, cur/next 블록 단일성 가드 - typeset.rs::paginate_table: 동일 패턴 적용 (실제 SVG 내보내기 경로) - 다중 행 블록이 페이지에 들어가지 않으면 블록 전체를 한 단위로 배치 본 샘플 검증: 1쪽에서 표 분할 사라지고 2쪽에 표 전체 시작. cargo test --lib: 1023 passed.
- cargo test --tests: 1073 passed (lib 1023 + integration 50) - svg_snapshot 골든 6건 통과 (table-text, issue-147/157/267, form-002, deterministic) - cargo build --release 성공 - 본 샘플 외 다른 표 샘플 (table-vpos-01, 표-텍스트) 정상 closes edwardkim#398
같은 paragraph 안에 TAC 컨트롤이 2개 이상 있을 때 두 번째 이후 그림의 pic_y가 paragraph 시작 y로 고정되어 표와 겹침. - pi=51 ci=0 (단독 그림): pic_y=94.49 (정상, 선행 TAC 없음) - pi=57 ci=1 (Table 뒤 그림): pic_y=578.09 (버그, y_offset=919.40 사용해야) 선행 TAC 존재 여부가 핵심 판별 조건임을 확인. Stage 2 구현 방향 확정.
같은 paragraph에 TAC 컨트롤(표/그림/도형) 2개 이상이 서로 다른 line_seg에 배치된 경우, 두 번째 이후 inline 그림이 첫 번째와 같은 y 좌표에 그려져 겹침/오버플로 발생하던 문제 수정. - layout.rs::layout_shape_item: 선행 TAC 컨트롤이 있으면 para_start_y를 진행된 y_offset으로 갱신하여 그림 y 좌표를 표 아래로 정확히 배치. - typeset.rs::typeset_table_paragraph: 선행 TAC 그림의 line_seg 높이를 current_height에 누적하고, 페이지 초과 시 다음 페이지로 분할. 기본 페이지네이션 엔진은 typeset.rs(TypesetEngine). engine.rs는 현재 RHWP_USE_PAGINATOR=1 fallback 경로이므로 typeset.rs만 수정. 회귀 테스트: 1023 passed, 0 failed. 샘플 비교: 7쪽 표 + 파이 차트 겹침 해소, 파이 차트가 8쪽 정상 배치.
closes edwardkim#402 검증 결과: - 7쪽: 표 + 파이 차트 겹침 해소 (PDF 일치) - 8쪽: 파이 차트 정상 배치 - cargo test 1023 passed, 0 failed - 10개 대표 샘플 LAYOUT_OVERFLOW 카운트 회귀 없음 - 페이지 수 27→30 (분할로 인한 정상 증가)
기존 task_404.md(영문 줄바꿈 역공학 미실행 초안)는 archives로 이동. 신규 task_404 = orphan heading vpos 기반 분할 추가.
TypesetState.page_first_vpos 필드 추가 + typeset_section 메인 루프에 진단 로그 삽입. 타겟 샘플 분석 결과: - pi=83 가설 확정: vpos overflow=886 HU, curr_h=906.6/avail=933.5 (cumulative fit) + next pi=84 표 190.9px fit 불가 → orphan - False positive 41건 중 40건은 wrap-around 페이지에서 vpos↔px 비율 어긋남 (페이지 8 pi=57 TAC 그림 + 빈 문단 19개) Stage 2 전략 재정의: heading-orphan 패턴 (current fit + next block 못 fit + vpos overflow + single column) 4조건 모두 만족 시 push. 단순 vpos overflow check은 다수 회귀 위험.
typeset_section 메인 루프에 vpos 기반 보정 추가. 5개 조건 AND (current fits + vpos overflow + next substantial + next doesn't fit + single column non-wrap) 으로 false positive 차단. 설계 변경: page_top_vpos 는 TypesetState 필드 대신 current_items 첫 item 의 para_index 로 즉시 계산 (typeset_paragraph 내부 페이지 flush 와 동기 안 되는 문제 회피). 검증: - pi=83 heading 이 페이지 9 → 페이지 10 으로 이동 (pi=84/85 표와 함께 배치) - 1073개 테스트 모두 통과 - 10개 샘플 LAYOUT_OVERFLOW: 회귀 없음 + 타겟 -15, kps-ai -1 개선
SVG 시각 검증으로 pi=83 "(7) 다수 기부자 현황" 이 페이지 10 첫 본문 라인으로 이동하고 후속 표 pi=84/85 와 함께 배치됨을 확인. 검증 기준 충족: 1. 페이지 9 SVG 에 pi=83 heading 미표시 2. 페이지 10 SVG 가 pi=83 + pi=84/85 표 함께 표시 3. 회귀 테스트 1073개 모두 통과 4. 10개 샘플 LAYOUT_OVERFLOW 회귀 없음 + 2개 샘플 개선 closes edwardkim#404
21페이지 2x1 표가 차트 아래로 ~400px 밀려나는 결함 분석. TopAndBottom Picture(vert=Para) 다음 문단의 vpos 보정에서 lazy_base 산출 시 차트 높이가 이중 반영되는 문제 진단. prev_has_overlay_shape 가드를 Control::Picture (non-TAC) + TopAndBottom/vert=Para 케이스로 확장하는 3단계 계획 수립.
차트 그림(pi=172, 170×111mm, TopAndBottom, vert=Para) 다음 문단의 vpos 보정에서 lazy_base가 차트 높이(31470 HU)만큼 낮게 산출되어 pi=174 (2x1 표) 가 y=948 → 표 자체 높이 합산 후 1049.7 로 밀려나 LAYOUT_OVERFLOW 21.8px 발생. 후속 17개 빈 문단(pi=175~191)도 연쇄 overflow + pi=192 (10x5 표) overflow 521.7px. 베이스라인: 1023 lib + 6 svg_snapshot 통과, LAYOUT_OVERFLOW 19건.
…opAndBottom/vert=Para) 기존 가드는 Control::Shape + InFrontOfText|BehindText 만 검사하여 Picture (그림) 컨트롤과 TopAndBottom 케이스를 처리하지 못함. Picture (non-TAC) 분기 추가 + TopAndBottom + vert_rel_to=Para 케이스 포함하도록 확장. 한컴이 후속 문단 vpos에 개체 높이를 반영하는 경우 sequential y_offset 이 이미 개체 바닥까지 진행된 상태에서 lazy_base 산출 시 prev_pi 텍스트 vpos_end 만 쓰면 차트 높이만큼 이중 점프 발생. 21페이지 LAYOUT_OVERFLOW 19건 -> 1건 (잔여는 별개 페이지네이션 결함). 2x1 표가 차트 바로 아래로 정상 위치 (PDF 21페이지와 일치). 1023 lib + 6 svg_snapshot 통과, 6개 샘플 무회귀.
cargo test --release 전체 10개 스위트 100% 통과 (1023 lib + 6 svg_snapshot + 통합 테스트, 실패 0). 10개 샘플 LAYOUT_OVERFLOW 비교: 6개 샘플 무회귀, 타겟 샘플 22→4 (-18) 개선. closes edwardkim#409
Task edwardkim#409 — prev_has_overlay_shape 가드를 Control::Picture (non-TAC) + TopAndBottom/vert=Para 케이스로 확장하여 차트 다음 문단/표가 차트 높이만큼 추가 점프하는 결함 해결. LAYOUT_OVERFLOW: 19 -> 1 (21페이지), 22 -> 4 (전체 문서). 1023 lib + 6 svg_snapshot + 통합 테스트 100% 통과. 6개 다른 샘플 무회귀.
prev_has_overlay_shape 가드를 Control::Picture (non-TAC) + TopAndBottom/vert=Para 케이스로 확장. LAYOUT_OVERFLOW: 22 -> 4, 21페이지 19 -> 1. 1023 lib + 6 svg_snapshot + 통합 테스트 100% 통과.
이슈 edwardkim#409 재오픈. v1 (layout 측 수정) 후 잔여: pi=191 헤딩 + pi=192 (10x5 표) 가 21페이지에 묶여 22페이지 SVG 에서 누락되는 결함. 근본 원인: typeset.rs::typeset_section 의 controls 루프가 비-TAC Picture/Shape 의 높이를 current_height 에 누적하지 않아 chart (419.6px) 가 페이지네이션에 미반영. Pagination 추정 used=803.3px (실제 layout y=1275.9px) → 모두 21페이지 packing. Stage 4: 컨트롤 루프 분기 확장 (TopAndBottom + vert=Para). Stage 5: 회귀 검증 + 통합 최종 보고서.
typeset.rs::typeset_section 의 controls 루프에 비-TAC + TopAndBottom + vert=Para 인 Picture/Shape 의 height + margin.bottom 을 current_height 에 누적하는 분기 추가. layout 의 calc_shape_bottom_y 와 동일한 산식. 22페이지에 (4) 헤딩 + 10x5 표 + 연령대별 차트 + 2x1 표가 PDF 와 동일하게 정상 출력. chart 관련 LAYOUT_OVERFLOW 전건 해소 (대상 샘플 4 -> 1, 잔여 1건은 본 변경과 무관한 기존 결함). 1023 lib + 6 svg_snapshot 통과, 6개 다른 샘플 무회귀.
전체 cargo test --release 11개 스위트 100% 통과 (실패 0). 6개 다른 샘플 LAYOUT_OVERFLOW 무회귀. 타겟 샘플 22 -> 1 (chart 관련 전건 해소). PR 초안을 v1 (layout) + v2 (pagination) 통합본으로 갱신. closes edwardkim#409
typeset.rs::typeset_section controls 루프에 비-TAC + TopAndBottom + vert=Para Picture/Shape 의 height + margin.bottom 을 current_height 에 누적. 22페이지 (4) 헤딩 + 10x5 표 정상 표시. LAYOUT_OVERFLOW (대상 샘플): v1 4 -> v2 1 (chart 관련 전건 해소). 1023 lib + 6 svg_snapshot + 9개 통합 = 11개 스위트 100% 통과. 6개 다른 샘플 무회귀.
typeset.rs::typeset_section 의 비-TAC TopAndBottom + vert=Para Picture/Shape 높이를 current_height 에 누적. 22페이지에 (4) 헤딩 + 10x5 표 정상 출력 (PDF 일치). 대상 샘플 LAYOUT_OVERFLOW 22 -> 4 (v1) -> 1 (v2). 11개 테스트 스위트 100% 통과, 6개 다른 샘플 무회귀. closes edwardkim#409
23페이지 차트(pi=208, TAC Picture, lh=23700 HU)가 SVG에서 24페이지로 밀리는 결함. 차트 시작 y=721.37 (vpos 1460593 -> +626.87px) 가 본문 안이지만 끝 1037.37 이 본문 1028 보다 9.37px 초과. PDF/HWP는 atomic (분할 불가 단일-line TAC) 항목에 대해 top-fit 시멘틱 사용 — 시작점이 본문 안이면 현재 페이지 배치 하고 하단 일부는 하단 여백(15mm)으로 흘림 허용. 우리는 strict bottom-fit 으로 판정 -> split -> 1-line 못 쪼갬 -> next page. Stage 6: typeset_paragraph 의 fit 분기에 atomic TAC top-fit 추가 Stage 7: 회귀 검증 + 통합 최종 보고서 v3
typeset_paragraph 의 fit 분기에 atomic TAC top-fit 추가. 단일 라인 + TAC Picture/Shape 항목은 시작점이 본문 안이고 끝이 60px (약 1.6cm) 이내 초과면 현재 페이지에 배치. HWP 의 atomic 항목 top-fit 시멘틱 (시작점이 본문 안이면 배치하고 하단 일부는 하단 여백으로 흘림 허용) 을 구현. 23페이지 차트(pi=208, lh=316px) 가 PDF 와 동일하게 표 아래에 정상 배치. 24페이지는 차트가 빠지고 정상 후속 콘텐츠로 시작. 대상 샘플 LAYOUT_OVERFLOW 동일 1건 유지 (chart 정상 배치 이므로 overflow 미발생). 6개 다른 샘플 무회귀. 1023 lib + 6 svg_snapshot 통과.
cargo test --release 11개 스위트 100% 통과 (실패 0). 6개 다른 샘플 무회귀. 타겟 샘플 LAYOUT_OVERFLOW: 22 -> 4 -> 1 -> 1 (chart 정상 배치). 21~24페이지 PDF 대조 전건 일치: - 21: 2x1 표 차트 직하 - 22: (4) 헤딩 + 10x5 표 - 23: 막대 차트 하단 - 24: 2x1 표 -> (6) 헤딩 -> 파이차트 closes edwardkim#409
typeset.rs::typeset_paragraph 의 fit 분기에 atomic TAC top-fit 추가. 단일 라인 + TAC Picture/Shape 항목은 시작점이 본문 안이고 끝이 60px 이내 초과면 현재 페이지에 배치 (HWP 시멘틱). 23페이지 차트(pi=208) 가 표 아래 정상 배치, 24페이지가 2x1 표 -> (6) 헤딩 -> 파이차트로 정상 시작 (PDF 일치). 11개 테스트 스위트 100% 통과, 6개 다른 샘플 무회귀.
23페이지 차트 정상 배치 + 24페이지 정상 콘텐츠 시작 (PDF 일치). 21~24 페이지 PDF 대조 전건 일치. closes edwardkim#409
- 수행 계획서, 구현 계획서, Stage 1 보고서 작성 - layout.rs:1422 분기에 RHWP_VPOS_DEBUG 진단 로그 추가 (env-gated) - 6개 샘플 page/lazy × applied/skip 분포 측정 - page_false 케이스를 4개 카테고리로 분류: A) 다단 우측 단 base≈7000 (target, 28건) ← fix 대상 B) vpos reset base≥60000 (합법 스킵, 20건) ← fix 후도 SKIP 유지 C) base=0 미세 drift (17건) ← 영향 없음 D) lazy_false 미세 drift ← 영향 없음 closes part of edwardkim#412 (Stage 1)
…irst_vpos 사용
핵심 변경 (layout.rs):
1. col_anchor_y = body_wide_reserved 푸시 직후 y_offset 캡처
- 첫 PageItem 의 실제 렌더링 y = vpos_page_base 좌표에 해당
- col_area.y 는 일반적으로 vpos=base 와 일치하지 않아 부정확
2. vpos_end 결정에 curr_first_vpos 우선 사용
- HWP 가 spacing_after 를 다음 paragraph first vpos 에 인코딩하므로
prev.vpos+lh+ls 보다 정확
- vpos reset 시에는 prev 기반 fallback
3. page_path / lazy_path 분리:
- page_path: col_anchor_y + (vpos_end - base) * scale
- lazy_path: col_area.y + (vpos_end - base) * scale (기존 유지)
검증 결과 (exam_eng.hwp):
- p1 우측 단 item 7 ①~⑤ 22.55 px 균일 (원래 보고된 버그 해결)
- p1 좌측 단 item 1 ①~⑤ 21.89 px 균일
- p2 우측 단 item 20 ①~⑤ 19.92 px 균일
- p2 우측 단 item 18 ①→② 잔존 (overlay shape bypass — 별도 task)
회귀:
- cargo test 6/6 통과
- golden snapshot: aift-page3 약 3.68 px shift (single ls 적용) — 의도된 개선,
issue edwardkim#147 본래 검증 목적과 무관해 골든 갱신
- issue-157: anchor 도입 후 자동 정상화
closes part of edwardkim#412 (Stage 2)
검증 대상 (7개 샘플 합계 268페이지): - exam_eng (8p), exam_kor (24p), exam_math (20p) - k-water-rfp (28p), 2025년 기부·답례품 (30p) - aift (78p), kps-ai (80p) 검증 결과: - cargo test 6/6 통과 - 단단 문서: 영향 없음 - 다단 문서: page_path 보정 정상화 (exam_kor 12건 false→true) - 시각 회귀 미관찰 (samples 1페이지 시각 비교 7건) 잔존 이슈: - exam_eng p2 item 18 ①→② (overlay shape bypass) → 별도 task 로 추적 권고 closes part of edwardkim#412 (Stage 3)
- mydocs/report/task_m100_412_report.md 작성 - mydocs/orders/20260428.md: edwardkim#412 항목 완료 갱신 closes edwardkim#412
|
여담입니다만 한 번도 얼굴을 뵙지는 못 했지만 제가 아키텍쳐의 길을 선택한 재미를 planet6897 님이 새삼 느끼게 해주셔서 감사드려요 ㅎㅎ |
|
따뜻한 코멘트 감사합니다. 사실 프로젝트가 쉽지 않아서 고생이 많으신데, 이런 어려운 프로젝트를 맡아주신 점 정말 높게 평가하고 싶습니다. 저도 메인테이너님께 힘이 될 수 있도록 제 위치에서 최선을 다해 돕겠습니다. 화이팅입니다! |
- mydocs/pr/pr_424_review.md (옵션 A: Task #412 본질 4 commits 분리 cherry-pick) - mydocs/pr/pr_424_report.md (cherry-pick 머지 결정 + SVG/Canvas 양 경로 시각 판정 통과) - mydocs/orders/20260426.md (PR #424 완료 행 추가) 검증: 1062 passed + svg_snapshot 6/6 + issue_418 1/1 + clippy 0 + WASM 4,184,640 bytes 광범위 byte 비교: 71 페이지 변화 (의도된 다단 vpos 보정 정정) 시각 판정: SVG + Canvas 양 경로 통과 (작업지시자 직접 한컴 정답지 비교)
|
@planet6897 님 PR 감사드립니다. 메인테이너가 cherry-pick 으로 devel 에 적용 완료했습니다. 처리PR 의 36 commits 중 Task #398/#402/#404/#409 commits 는 이미 다른 PR 로 흡수 완료 + 샘플/계획서/Stage/merge 제외하고, Task #412 본질 4 commits 만 분리 cherry-pick — 작성자 attribution 보존:
devel 머지 commit: 검증
광범위 byte 단위 비교10 샘플 / 309 페이지 SVG 비교 결과:
차이 분포: exam_* (eng+kor+math) 46 페이지, kps-ai 17, aift 4, synam-001 3, 2025년 기부 1. 시각 판정 (작업지시자 직접)본 PR 은 layout.rs 의 vpos 보정 공식 자체를 변경하므로 다단 레이아웃 페이지에 광범위 변화가 발생합니다. 작성자 명시 핵심 외에 다른 71 페이지의 변화가 회귀인지 의도된 정정인지 작업지시자가 한컴 정답지와 SVG + Canvas 양 경로 직접 비교 후 판정 진행했습니다.
메모리 원칙 부합본 PR 의 변경은 이슈 #412 도 함께 close 됩니다. 감사합니다. |
Summary
layout.rsvpos 보정 공식이col_area.y와vpos_page_base의 좌표 의미를 혼동했던 부분을 정리closes #412
원인
layout.rs:1392-1430의 vpos 보정 공식:col_area.y가 vpos=0 좌표를 의미한다고 가정한 공식이지만,body_wide_reserved(페이지 너비 글상자/표) 푸시가 있는 다단 레이아웃에서 첫 항목은col_area.y가 아닌 푸시 적용 후 위치에서 시작.다단 우측 단처럼
vpos_page_base가 큰 경우, 보정값col_area.y + (vpos_end - base) * scale이 sequentialy_offset보다 항상 작아져 조건 검사를 통과하지 못하고 스킵 → 단행 문단의 trailing line_spacing 이 누락된 채로 렌더.변경 사항
src/renderer/layout.rs:anchor 캡처:
build_single_column진입 시 body_wide_reserved 푸시 직후의y_offset을col_anchor_y로 보존. 첫 PageItem 의 실제 렌더 y =vpos_page_base좌표에 대응.vpos_end 정밀화:
prev_seg.vpos + lh + ls대신 현재 paragraph 의 first seg vpos 를 우선 사용. HWP 가 paragraph spacing_after 를 다음 paragraph 의 first vpos 에 인코딩하므로 더 정확. vpos reset(0) 또는 prev 보다 작아진 경우 prev 기반 fallback.page_path / lazy_path 분리:
col_anchor_y + (vpos_end - base) * scalecol_area.y + (vpos_end - base) * scale(lazy_base 가 sequential y_offset 으로부터 역산되어 col_area.y 기준이 일관)진단 코드 (env-gated):
RHWP_VPOS_DEBUG=1시 보정 분기 동작 추적용 eprintln.검증 결과
원래 보고된 버그 (exam_eng p1 우측 단 item 7)
추가 수정
분포 변화 (exam_eng)
→ 모든 page_path 보정이 정상 발동.
회귀 검증 (7개 샘플 268페이지)
잔존 이슈 (별도 task 권고)
exam_eng p2 우측 단 item 18 ①→② (Δ=15.33, 기대 19.93):
prev_has_overlay_shape+prev_tac_seg_applied두 가드로 차단Test plan
단계별 산출물
mydocs/plans/task_m100_412.mdmydocs/plans/task_m100_412_impl.mdmydocs/working/task_m100_412_stage1.mdmydocs/working/task_m100_412_stage2.mdmydocs/working/task_m100_412_stage3.mdmydocs/report/task_m100_412_report.md