Skip to content

Task #452: 단락 마지막 줄 trailing line_spacing 정합 (exam_kor pi=1↔pi=2)#454

Closed
planet6897 wants to merge 21 commits into
edwardkim:develfrom
planet6897:local/task452
Closed

Task #452: 단락 마지막 줄 trailing line_spacing 정합 (exam_kor pi=1↔pi=2)#454
planet6897 wants to merge 21 commits into
edwardkim:develfrom
planet6897:local/task452

Conversation

@planet6897

Copy link
Copy Markdown
Contributor

Summary

문제

samples/exam_kor.hwp 1페이지 좌측 단에서 pi=1("밑줄 긋기는 일상적으로...") → pi=2("통상적으로 독자는...") 전환 간격이 단락 내 줄 간격보다 좁게 렌더링됨.

측정
pi=1 단락 내 step 24.51 px
pi=1.line9 ↔ pi=2.line0 step 15.34 px ← 9.17 px (= 1 ls = 688 HU) 부족
기대값 (PDF 정합) 24.51 px (1838 HU)

PDF (samples/exam_kor.pdf) 측정값: 모든 줄 step 12.96 pt 균일 (= 1838 HU).

원인

src/renderer/layout/paragraph_layout.rs:2511-2520is_para_last_line 분기가 단락 마지막 visible 줄에서 trailing line_spacing 을 제외하고 y 를 lh 만 전진. 이 y 가 layout_paragraph 반환값 → 다음 단락 y_start 가 되어 다음 단락 첫 줄이 1 ls 만큼 위로 당겨짐.

Task #332 stage 2 의 의도(typeset 의 height_for_fit 와 layout 정합)가 layout 만 trailing 제외 → pagination/engine 의 current_height += para_height 누적과 1 ls drift 발생한 절반의 정합. 본 PR 은 layout 도 trailing 포함으로 통일하는 반대 방향 정합.

수정 (코드)

src/renderer/layout/paragraph_layout.rs:2511-2520:

// Before
let is_cell_last_line = is_last_cell_para && line_idx + 1 >= end;
let is_para_last_line = cell_ctx.is_none()
    && line_idx + 1 == end
    && end == composed.lines.len();
if (is_cell_last_line && cell_ctx.is_some()) || is_para_last_line {
    y += line_height;
} else {
    let line_spacing_px = hwpunit_to_px(comp_line.line_spacing, self.dpi);
    y += line_height + line_spacing_px;
}

// After
let is_cell_last_line = is_last_cell_para && line_idx + 1 >= end;
if is_cell_last_line && cell_ctx.is_some() {
    y += line_height;
} else {
    let line_spacing_px = hwpunit_to_px(comp_line.line_spacing, self.dpi);
    y += line_height + line_spacing_px;
}

검증 결과

정량 (exam_kor 1페이지)

측정 Before After 기대
pi=1.line9 ↔ pi=2.line0 step 15.34 px 24.50 px 24.51 px
단락내 step 24.51 px 24.50 px 24.51 px ✓

자동 테스트

  • cargo test --lib --release: 1066 passed (회귀 0)
  • cargo test --release --test svg_snapshot: 6/6 passed (UPDATE_GOLDEN=1 으로 2건 baseline 재갱신)

페이지 수 회귀 (10 종 샘플)

샘플 Before After
exam_kor.hwp 20 20
aift.hwp 77 77
biz_plan.hwp 6 6
2022년 국립국어원 업무계획.hwp 40 40
exam_eng.hwp 8 8
exam_math_8.hwp 1 1
k-water-rfp.hwp 28 28
kps-ai.hwp 80 80
synam-001.hwp 35 35
21_언어_기출_편집가능본.hwp 15 15

10/10 샘플 페이지 수 동일.

Task #332 회귀 점검

samples/21_언어_기출_편집가능본.hwp page 1 col 1 의 pi=26 + 보기 ①②③ (pi=27, pi=28, pi=29) 모두 page 1 col 1 에 fit. #332 회귀 0.

이론적 안전성: pagination engine 의 fit 판정은 effective_trailing (마지막 단락 trailing ls 만 fit 시 제외) 사용 → 본 수정으로 fit 판정 로직 자체는 변하지 않음. layout y 시프트만 정합.

golden SVG snapshot

Task #332 stage 5 에서 갱신된 baseline → 본 정합 baseline 으로 재갱신:

  • tests/golden_svg/issue-147/aift-page3.svg (TOC 페이지)
  • tests/golden_svg/issue-157/page-1.svg (등기 양식 페이지)

PNG 시각 검토: 두 페이지 모두 콘텐츠 정상 표시, 잘림/겹침/누락 없음.

부수 효과

  • LAYOUT_OVERFLOW 경고 1건 (issue-157 pi=28, 10.9 px 오버플로): 페이지 마지막 단락의 trailing ls 가 col_bottom 을 살짝 넘는 cosmetic 효과. 빈 공간이므로 시각 무영향.
  • 모든 본문 단락이 있는 페이지의 SVG y 좌표가 trailing ls 누적분만큼 시프트 다운. 콘텐츠/구조 변화 0.

Test plan

단계별 커밋

관련

  • 수행계획서: mydocs/plans/task_m100_452.md
  • 구현 계획서: mydocs/plans/task_m100_452_impl.md
  • 단계별 보고서: mydocs/working/task_m100_452_stage{1,2,3}.md
  • 최종 보고서: mydocs/report/task_m100_452_report.md

planet6897 and others added 21 commits April 29, 2026 11:54
exam_kor.hwp 24페이지 → 20페이지 정합 작업 1단계.

- 페이지별 단별 used/hwp_used/diff CSV (output/debug/task435/)
- 회귀 대상 5문서 페이지 수 (exam_kor 24, exam_eng 8, k-water-rfp 28, hwpspec 177, synam-001 35)
- RHWP_TYPESET_DRIFT 출력 캡처 (pi=0.30, pi=1.25 split 진단)
- compute_body_wide_top_reserve_for_para 산정 경로 추적

핵심 진단: col 1 reserve 306.1px (HWP 실제 94.5px 대비 +211.6px 과대).
원인: Paper-rel 좌표를 body-rel 변환 없이 그대로 reserve 에 누적.
Stage 2 에서 typeset.rs:2127-2172 의 VertRelTo::Paper 분기 정정.

수행계획서/구현계획서/Stage 1 보고서 포함.
compute_body_wide_top_reserve_for_para 의 VertRelTo::Paper 분기에서
body-rel 변환 누락 정정. body 와 일부만 겹치는 (header→body 침범)
케이스에서 Paper-rel 좌표를 그대로 reserve 에 누적하던 버그 수정.

수정 전 reserve = shape_y_offset(paper) + h + outer_bottom = 306.1 px
수정 후 reserve = max(0, shape_top_abs - body_top) ... = 94.4 px

결과:
- exam_kor.hwp: 24 → 22 페이지 (page 2, 15 orphan 해소)
- pi=0.30, pi=1.25 split → FullParagraph
- 회귀: exam_eng 8, k-water-rfp 28, hwpspec 177, synam-001 35 유지
- cargo test: 1062 passed

Stage 3 에서 일반 페이지 누적 -100~-300px 정정 (22 → 20).
원래 가설 ("표/도형 후 컬럼 잔여 공간 산정 부족") 재검토.
RHWP_TYPESET_DRIFT 분석 결과 diff 메트릭은 typeset cur_h 누적
(height_for_fit, trail_ls 제외) vs hwp_used (last line vpos+lh)
의 좌표계 차이를 측정하는 것일 뿐, "rhwp 가 채울 수 있는데 못 채운
잔여 공간" 이 아님을 확인.

실제 22→20 페이지 단축 장애물:
1. 섹션 1 페이지 14: Square wrap 표 + col 0 over-fill (1225>1211)
   → col 1 under-use (64px)
2. 섹션 1 페이지 15: 단일 컬럼 출력 (단정의는 2단인데 단 1 누락)
3. 섹션 2 페이지 18: pi=11 split + pi=13 [단나누기] orphan-like

3가지 전부 해결 시 22→19 가능 (목표 20 도달).

옵션 A/B/C 결정 필요 (현 상태 종료 / Stage 4 확장 / Stage 4 부분).
옵션 A 종료: 24→22 페이지 (Stage 2 col 1 reserve 정정).
잔여 22→20 미달성, 3가지 별도 메커니즘 (Square wrap over-fill,
단일 컬럼 출력 버그, col 0 cur_h over-advance) 별도 task 분리 권고.

edwardkim#393 (옵션 A) 본 task Stage 2 로 적용 완료, close 가능.
수행계획서(task_m100_439.md) + 단계1 진단 보고서.

핵심 발견:
- 이슈 가설의 engine.rs:702-711 는 fallback 경로 (RHWP_USE_PAGINATOR=1).
  기본 활성 엔진은 typeset.rs::TypesetEngine.
- 실제 버그 위치: typeset.rs::place_table_with_text (1400-1467).
  engine.rs 의 !is_wrap_around_table 가드 (engine.rs:1349, 1422) 누락.
- 페이지 14 col 0 used=1225.8 (본문 1211.3 초과 +14.5) 정확히 재현.

코드 변경 없음 — 임시 디버그 코드는 모두 revert.
place_table_with_text 의 정확한 cur_h 누적 추적 (디버그 후 revert):
- Square wrap 4 표가 pre_height + table_total 합산 → +244.88 px 과다
- HWP 의도: max(호스트 텍스트, 표 + v_offset) 만 누적

수정안: typeset.rs::place_table_with_text 에서
  current_height += max(pre_height, v_off + table_total) (Square wrap 시)

코드 변경 없음 — 임시 디버그 println 모두 revert.
typeset.rs::place_table_with_text 에서 Square wrap (어울림) 표일 때
current_height 누적을 pre_height + table_total → max(pre_height, v_off + table_total)
로 변경.

호스트 문단 텍스트와 어울림 표는 같은 수직 영역을 공유하므로
더 큰 쪽만 한 번 누적해야 HWP layout 의도와 일치.
PageItem 자체는 PartialParagraph + Table 모두 push (layout 렌더링 보존).

검증:
- 페이지 14 col 0 used 1225.8 → 1036.1 px (≤ 1211.3 충족)
- exam_kor.hwp 22 → 20 페이지 (목표 ≤ 21 충족)
- 회귀 샘플 6 종 (exam_eng/math, 21언어, aift, 2010-01-06, biz_plan) 페이지 수 동일
- cargo test 1066 개 모두 통과
- SVG 렌더링 정상

closes edwardkim#439
Stage 4 회귀 검증 결과:
- 149 개 sample HWP 전수 페이지 수 비교: exam_kor 만 22→20 변경, 148개 동일
- cargo test 1066 passed
- DoD 전부 충족

본 task 완료. closes edwardkim#439.
typeset.rs::place_table_with_text 의 Square wrap 표 누적 정책을
pre_height + table_total → max(pre_height, v_off + table_total) 로 변경.

효과:
- exam_kor.hwp 페이지 14 col 0 used 1225.8 → 1036.1 px (over-fill 해소)
- exam_kor.hwp 22 → 20 페이지
- 149 개 sample 중 exam_kor 만 변화, 148 개 동일 (회귀 0건)
- cargo test 1066 passed

closes edwardkim#439
…문제 수정

- 수행/구현 계획서 + Stage 1·2 보고서 작성
- src/renderer/layout.rs: paragraph border merge 그룹을 col_area 바닥/꼭대기로 클램프
- exam_kor p2/5/8/15 의 세로 구분선이 PDF 와 일치하는 길이로 정상화
  (p8: 1671 → 1425, 246px 단축, 페이지 바깥 침범 해소)
- vpos-reset 미존중으로 인한 텍스트 자체의 overflow 는 별도 이슈로 분리
- snapshot 갱신: tests/golden_svg/issue-267/ktx-toc-page.svg
  (invisible 구조 rect 의 height 5.34px 변화, 가시 변화 없음)
페이지 번호 박스가 column divider line 과 붙어 보이는 문제 해결.

원인: 꼬리말 paragraph 의 vert=Para + wrap=TopAndBottom 표가 paragraph
top 에 배치되어 본문 바닥과 같은 y 에 위치. HWP 의 실제 동작은 첫 라인의
line_height/2 만큼 아래에 anchor 되어 본문과 시각적 갭 형성.

수정: layout_header_footer_paragraphs 에서 해당 조건의 첫 paragraph 표는
y_offset 에 line_height/2 (px) 를 더하여 배치.

검증 (exam_kor.hwp 20p):
- 박스 top y: 1422.93 → 1439.47 (PDF 380.6mm 와 일치)
- column line - 박스 갭: 0px → 16.3-17.0px (PDF 16.0px 와 일치)
- column line 길이/위치는 PDF 자연 그대로 유지 (p1 1131px, p2+ 1226px)
- cargo test --release: 1117 passed, 0 failed
이슈 edwardkim#445 의 두 시각적 결함 (paragraph border 페이지 바깥 침범 + 페이지
번호 박스가 column line 에 붙음) 모두 PDF 와 일치하도록 수정 완료.

승인 후 local/devel 머지 + edwardkim#445 close 예정.
- 수행계획서 (mydocs/plans/task_m100_452.md): 옵션 A 선언, 4단계 분해
- 구현 계획서 (mydocs/plans/task_m100_452_impl.md): paragraph_layout.rs:2511-2520
  is_para_last_line 분기 제거, is_cell_last_line 만 보존
- Stage 1 보고서 (mydocs/working/task_m100_452_stage1.md):
  - exam_kor pi=1.line9↔pi=2.line0 step = 15.34px (버그) ↔ 단락내 24.51px
  - 10종 샘플 페이지 수 baseline 캡처 (/tmp/task_452_baseline/)
  - 21_언어 p1 col 1 pi=26+보기①②③ fit 확인 (edwardkim#332 회귀 baseline)
- orders 갱신: 버그 섹션 edwardkim#452 항목 추가
… + golden 갱신

- src/renderer/layout/paragraph_layout.rs:2511-2519: is_para_last_line 분기
  제거. is_cell_last_line(셀 내) 만 trailing 제외 보존, 본문 단락은 모든 줄에서
  y += lh + ls 통일. pagination/engine.rs 의 current_height 누적과 정합.
- 검증: exam_kor 1페이지 pi=1.line9↔pi=2.line0 step = 15.34 → 24.50 px
  (단락내 step 과 동일). cargo test --lib 1066 passed.
- golden SVG 2건 baseline 갱신 (Task edwardkim#332 에서 갱신된 것을 본 정합으로 재갱신):
  - tests/golden_svg/issue-147/aift-page3.svg
  - tests/golden_svg/issue-157/page-1.svg
- LAYOUT_OVERFLOW 메시지: 페이지 마지막 단락의 trailing ls (~10.9 px)
  가 col_bottom 을 살짝 넘으나 빈 공간이므로 시각 무영향. pagination
  engine 의 effective_trailing 처리로 페이지 분배는 유지.
edwardkim added a commit that referenced this pull request Apr 29, 2026
- mydocs/pr/pr_454_review.md (Task #452 cherry-pick 검토, 광범위 영향 87% 정황)

검증: 1069 passed + svg_snapshot 6/6 (golden 2건 갱신) + issue_418 1/1 + clippy 0
광범위 byte 비교: 305 중 266 차이 (87% 영향, paragraph_layout 변경)
시각 판정: 후속 PR (#457, #461) 처리 후 통합 검증 (작업지시자 결정)
본질: paragraph_layout::is_para_last_line 분기 제거 (셀 마지막 줄만 trailing 제외 보존)
edwardkim added a commit that referenced this pull request Apr 29, 2026
edwardkim added a commit that referenced this pull request Apr 29, 2026
@edwardkim

Copy link
Copy Markdown
Owner

@planet6897 님 PR 감사드립니다. 메인테이너가 cherry-pick 으로 devel 에 적용 완료했습니다.

본 사이클 14번째 PR 입니다. PR #450 머지 후 후속 본질 정정 흐름을 정확히 이어가고 계십니다.

처리

작성자 attribution 보존 본질 4 commits 분리 cherry-pick:

devel 머지 commit: fa8c394

검증

광범위 byte 비교

10 샘플 / 305 페이지 SVG 비교: 39 동일, 266 차이 (87.2% 영향).

차이 분포: kps-ai 71, aift 70, exam_* 48, 2025년 기부 25, k-water-rfp 23, synam-001 22, biz_plan 6, equation-lim 1.

→ paragraph_layout 의 단락 마지막 줄 처리 변경이라 본문 단락이 있는 모든 페이지에 영향. 작성자 PR 본문은 exam_kor 1페이지의 pi=1↔pi=2 step 정합만 명시했지만 실제 영향은 광범위.

시각 판정 정책 (작업지시자 결정)

본 PR 단독 시각 판정 보류. 후속 PR (#457 Task #455, #461 Task #459/#462/#463/#468/#469) 가 같은 영역 (paragraph_layout / vpos / col_bottom) 에 누적 정정 진행 정황이라, 모든 PR 처리 후 통합 시각 검증 으로 결정.

이는 메모리 feedback_small_batch_release_strategy 의 빠른 회전 정책 + feedback_v076_regression_origin 의 광범위 변화 직접 시각 검증 게이트 균형.

본 PR 의 좋은 점

  1. 정확한 본질 진단: is_para_last_line 분기가 Task typeset/layout drift 통합 — 단일 advance 모델로 정합 (Task #331 재시도 기반) #332 stage 2 의 의도와 어긋나 1 ls drift 발생을 정확 식별
  2. PDF 정량 측정: pi=1↔pi=2 step 15.34→24.50 px (24.51 px 단락 내 step 과 정확 일치)
  3. 셀 마지막 줄 보존: is_cell_last_line && cell_ctx.is_some() 조건은 그대로 유지 — 셀 높이 모델 영향 없음 (메모리 feedback_hancom_compat_specific_over_general 부합)

이슈 #452 도 함께 close 됩니다. 다음 PR (#457 Task #455) 처리도 같은 사이클로 이어가겠습니다. 감사합니다.

@edwardkim edwardkim closed this Apr 29, 2026
edwardkim added a commit that referenced this pull request Apr 29, 2026
- mydocs/pr/pr_461_review.md (5 Tasks 분리 cherry-pick 검토, 광범위 영향 86% 정황)

검증: 1070 passed + svg_snapshot 6/6 (golden 4건 갱신) + issue_418 1/1 + clippy 0
광범위 byte 비교: 305 중 263 차이 (86% 영향, Task #463 깊은 누적 정정)
시각 판정: PR #454 + #457 + #461 통합 검증 (작업지시자 결정)

본질 5 Tasks:
- Task #459: 다단 후속 페이지 vpos-reset (PR #450 잔여 본질)
- Task #462: TAC Picture 인라인 line advance
- Task #463: 셀 leakage + 박스 geometry + 들여쓰기 + TAC crop + 바탕쪽 (Stage 1~8)
- Task #468: cross-column 박스 partial 플래그
- Task #469: cross-column partial 박스 col_top/col_bot 침범
@planet6897 planet6897 deleted the local/task452 branch April 30, 2026 00:02
edwardkim added a commit that referenced this pull request Apr 30, 2026
- mydocs/pr/pr_456_review.md (P2 cherry-pick 검토, SVG 100% byte 동일)

검증: 1075 passed (+5 Canvas parity test) + svg_snapshot 6/6 + issue_418 1/1 + clippy 0
WASM: 4,206,022 bytes (+19,741, paint 모듈 Canvas replay 추가)
광범위 byte 비교: 305/305 byte 동일 (SVG legacy 경로 0 영향) ✅

본질 (PR #419 의 P2):
- Canvas 렌더 경로를 PageLayerTree replay 로 전환
- legacy 경로는 renderPageCanvasLegacy 로 보존 (fallback)
- LayerBuilder leaf children 보존 정정
- Canvas parity test 추가 (CI 통합)

시각 판정: 통합 검증 (PR #454 + #457 + #461 + #456 머지 후 작업지시자 직접)
@planet6897 planet6897 restored the local/task452 branch May 3, 2026 23:36
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