diff --git a/mydocs/plans/task_m100_853.md b/mydocs/plans/task_m100_853.md new file mode 100644 index 000000000..158bdb872 --- /dev/null +++ b/mydocs/plans/task_m100_853.md @@ -0,0 +1,48 @@ +# 수행 계획서 — Task #853 (M100) + +GitHub Issue: edwardkim/rhwp#853 · 브랜치: `local/task853` (← upstream/devel `2bd50a3a`) + +## 1. 배경 + +`samples/basic/shortcut.hwp` ↔ 한컴 PDF `pdf/basic/shortcut-2022.pdf` 시각 비교에서 두 증상: + +1. **모든 구분 칸 위·아래 줄 간격 압축** — 섹션 헤더 띠(1×1 TAC 표: 커서 이동·지우기·파일·편집·보기·입력·서식·쪽·도구·표·기타)와 `<…에서>` 부제목 줄(미리 보기·편집 화면 분할·그림 넣기·글상자·상용구·스타일·글자 속성·문단 속성·개요 번호·매크로·F5 셀 블록·그림 그리기) 주변 수직 여백이 한컴 대비 ~20px 부족. +2. **일부 페이지에서 본문영역(body_area) 하단 초과 렌더링** — 3쪽 SVG 콘텐츠 max y ≈ 766px > 본문영역 하단 758.4px. 단0~단18(19개 zone)이 쌓여 마지막 zone zone_y_offset=720.2px > 본문영역 높이 701.7px. + +부수 결함(이미 관측): 3쪽 단3에서 `<편집 화면 분할에서>`(pi=94)와 "화면 이동"(pi=95)이 둘 다 vpos=0 으로 겹침 — 닫힌 #768(다단 zone 분할 행 밀림) 패턴. + +본 사안은 Task #842 결함 #1(헤더 1×1 TAC 표 앞뒤 spacing 압축, 1쪽만 검토 후 보류)의 전면화 + 본문영역 초과 증상 추가. + +## 2. 원인 가설 (Stage 1 진단 대상) + +- 한컴은 다단 zone 전환(1단↔2단 `다단나누기`)과 TAC 표 anchor 문단 주변에 명시 spacing(before/after)에 없는 **암묵 수직 간격(~20~27px)** 을 넣음 → rhwp 미재현 → 띠가 짧음 → 시각 압축 + 페이지네이터 zone 과적재 → 본문영역 초과. +- 페이지네이터가 누적 offset의 body_bottom 초과 시 다음 페이지 break 검사가 TAC 표 띠 + 다단 zone 조합에서 작동 안 함(별개 가능성). +- 3쪽 pi=94/95 vpos=0 겹침(#768 패턴)이 3쪽 overflow 에 직접 기여. + +→ Stage 1 에서 코드 경로 특정: `src/renderer/layout.rs`(zone 배치, `build_column_separators`, `start_new_column_band`, `layout_table_item` is_tac 분기, 페이지 break), `src/renderer/layout/paragraph_layout.rs`, `src/document_core/` LINE_SEG vpos 해석. 한컴 spacing 출처(zone 전환 / TAC 표 line-height / LINE_SEG vpos)를 PDF·IR 대조로 규명. + +## 3. 위험 평가 + +- `feedback_essential_fix_regression_risk` 정합 — 다단 / 단일 단 / 표분할 상호작용으로 회귀 위험 큼. → 광역 회귀(전 fixture sweep) + `cargo test`(svg_snapshot 포함) + 한컴 2010/2020 정답지 비교 동반 필수. +- `feedback_rule_not_heuristic` 정합 — 한컴 암묵 spacing 규칙이 명세화 가능한 룰인지, 휴리스틱 보정인지 자문 후 적용. +- RFC #774("한컴 PDF paragraph spacing 알고리즘 정밀 분석") 영역 — 본 타스크에서 RFC 를 어디까지 흡수할지는 작업지시자 판단 요청(현 계획서는 shortcut.hwp 한정 정합 + RFC 는 별도 유지로 가정). + +## 4. 산출물 + +- 구현 계획서 `mydocs/plans/task_m100_853_impl.md` (3~6단계) +- 단계별 완료보고서 `mydocs/working/task_m100_853_stage{N}.md` +- 최종 결과보고서 `mydocs/report/task_m100_853_report.md` +- (필요 시) 진단/기술 정리 `mydocs/tech/` 또는 `mydocs/troubleshootings/` + +## 5. 검증 기준 + +- shortcut.hwp 7쪽 SVG ↔ `pdf/basic/shortcut-2022.pdf` 시각 정합: 구분 칸 위·아래 여백 PDF 대비 ±6px 수렴, 본문영역 초과(콘텐츠 y > body_bottom) 해소. +- `cargo test` 전건 통과(svg_snapshot 포함), 회귀 0. +- 광역 fixture sweep: shortcut.hwp 외 변경 영향 없음(byte-identical 또는 의도된 변경만). +- 3쪽 pi=94/95 vpos 겹침: 본 타스크 범위 포함 여부는 구현 계획서에서 확정(#768 재오픈 vs 본 타스크 흡수). + +## 6. 승인 요청 사항 + +1. 본 수행 계획 진행 승인. +2. RFC #774 포함 범위 — (a) shortcut.hwp 한정 정합만 / (b) RFC #774 일부 분석 흡수 / (c) RFC #774 우선 별도 진행. +3. 3쪽 pi=94/95 vpos 겹침(#768) 처리 — (a) 본 타스크 흡수 / (b) #768 재오픈 후속 분리. diff --git a/mydocs/plans/task_m100_853_impl.md b/mydocs/plans/task_m100_853_impl.md new file mode 100644 index 000000000..a5fa38f8c --- /dev/null +++ b/mydocs/plans/task_m100_853_impl.md @@ -0,0 +1,48 @@ +# 구현 계획서 — Task #853 (M100) + +GitHub Issue: edwardkim/rhwp#853 · 브랜치: `local/task853` (← upstream/devel `2bd50a3a`) +수행 계획서: `mydocs/plans/task_m100_853.md` + +## 전제 (작업지시자 승인 — 기본값 적용) + +- RFC #774 범위: **(a) shortcut.hwp 한정 정합만** 수행. RFC #774 의 일반 알고리즘 분석은 별도 유지. +- 3쪽 pi=94/95(`<편집 화면 분할에서>` ↔ "화면 이동") vpos=0 겹침: **#768 영역으로 분리** 가정. 단 Stage 1 진단에서 본 타스크 수정과 동일 근원으로 밝혀지면 범위 흡수(Stage 1 보고서에서 확정). + +## 단계 구성 (5단계) + +### Stage 1 — 진단 (코드 미수정) +- `dump-pages` / `dump` / `ir-diff`(필요 시 hwpx 변환본) / `export-svg --debug-overlay` 로 shortcut.hwp 1·2·3쪽 zone 배치·LINE_SEG vpos·TAC 표 outer_margin·anchor 문단 PS 정밀 관측. +- `pdf/basic/shortcut-2022.pdf` 와 px 단위 대조: ① 구분 칸 띠 위 여백 ② 띠 아래 → 첫 본문행 여백 ③ 제목 위 여백 ④ zone 전환(1단↔2단 `다단나누기`) 시 삽입 간격. +- 한컴 암묵 수직 간격의 **출처 후보 판정**: (가) zone 전환 시 고정/비율 간격, (나) TAC 표 anchor 문단 line-height(`line=100%`) 기반, (다) LINE_SEG `vpos`/`lh`/`th` 해석 누락, (라) 표 outer_margin top/bottom 적용 누락. +- 영향 코드 경로 특정: `src/renderer/layout.rs`(zone 배치 누적 offset, `start_new_column_band`, `layout_table_item` is_tac 분기, 페이지 break 판정), `src/renderer/layout/paragraph_layout.rs`, `src/document_core/` LINE_SEG. +- 3쪽 본문영역 초과(콘텐츠 y > body_bottom 758.4) 가 (1) 띠 압축으로 zone 과적재의 결과인지 (2) 페이지 break 판정 자체의 버그인지 분리. +- 산출: `mydocs/working/task_m100_853_stage1.md` — 진단 결과 + 규명된 근원 + (필요 시) 구현 계획 v2 간소화 + #768 흡수 여부 결정. + +### Stage 2 — 구분 칸 암묵 수직 간격 재현 +- Stage 1 에서 규명된 근원에 한해 최소 수정. 후보 우선순위: (라) 표 outer_margin top/bottom → (가) zone 전환 간격 → (나) TAC anchor line-height. +- shortcut.hwp 의 TAC 표 띠(`size=...×1766` 6.2mm, outer_margin 1mm, anchor PS `line=100%`)와 `다단나누기` zone 전환 양쪽에 적용. +- 변경 후 shortcut.hwp 7쪽 `export-svg` → 구분 칸 위·아래 여백 PDF 대비 ±6px 수렴 확인. +- 산출: `mydocs/working/task_m100_853_stage2.md` + 소스 커밋. + +### Stage 3 — 페이지 break 판정 보강 (필요 시) +- Stage 2 적용 후에도 본문영역 초과(콘텐츠 y > body_bottom)가 남으면, 누적 zone offset 이 body_bottom 초과 시 다음 페이지로 break 하도록 `src/renderer/layout.rs` 페이지 break 판정 보강. +- TAC 표 띠 + 다단 zone 조합에서 break 검사가 우회되는 경로 차단. +- Stage 2 만으로 초과가 해소되면 본 단계는 "수정 불요" 보고로 마감. +- 산출: `mydocs/working/task_m100_853_stage3.md` (+ 소스 커밋 또는 불요 사유). + +### Stage 4 — 광역 회귀 검증 +- `cargo test`(svg_snapshot 8/8 포함) 전건 통과 확인. +- `cargo clippy --lib -- -D warnings` (pre-existing 무관 오류는 명시). +- 광역 fixture sweep(전 샘플 SVG 내보내기) → shortcut.hwp 외 변경 영향 0(byte-identical) 또는 의도된 변경만임을 입증. +- shortcut.hwp 7쪽 SVG ↔ `pdf/basic/shortcut-2022.pdf` 시각 정합 최종 확인. (한컴 2010/2020 환경 차이는 `feedback_pdf_not_authoritative` 정합으로 함께 점검 — macOS 환경이라 `pdf/`(2022) 1차, 가능 시 `pdf-2020/` 보조.) +- 산출: `mydocs/working/task_m100_853_stage4.md` + 소스 커밋(있다면). + +### Stage 5 — 최종 결과보고서 +- 결과 요약(증상1/2 해소 여부), 변경 내역, 회귀 검증 결과, 잔여 사항(#768 등). +- 산출: `mydocs/report/task_m100_853_report.md` + 커밋. (mydocs/orders 는 수정하지 않음 — 작업지시자 거버넌스 영역.) + +## 회귀 가드 요약 + +- 매 단계 `cargo test` 통과 필수, Stage 4 광역 sweep 필수. +- 본질 정정(zone 배치/페이지 break)이므로 다단·단일 단·표분할 상호작용 회귀를 sweep 으로 입증(`feedback_essential_fix_regression_risk`). +- 한컴 spacing 규칙은 룰/휴리스틱 구분 후 적용(`feedback_rule_not_heuristic`). diff --git a/mydocs/plans/task_m100_853_impl_v2.md b/mydocs/plans/task_m100_853_impl_v2.md new file mode 100644 index 000000000..a8e08762f --- /dev/null +++ b/mydocs/plans/task_m100_853_impl_v2.md @@ -0,0 +1,41 @@ +# 구현 계획서 v2 (Stage 1 진단 반영) — Task #853 (M100) + +GitHub Issue: edwardkim/rhwp#853 · 브랜치: `local/task853` +원본 구현 계획서: `mydocs/plans/task_m100_853_impl.md` · Stage 1 진단: `mydocs/working/task_m100_853_stage1.md` + +## 확정 사항 (작업지시자 승인 — "진행"으로 기본값 채택) + +- Stage 2 = **옵션 A 변형**: `is_column_top` 시 `spacing_before` 를 통째 드롭하지 않고 **해당 문단의 첫 LINE_SEG `vpos`(한글이 실제 렌더한 위치)로 클램프** — `applied_before = min(spacing_before, hwpunit_to_px(line_segs[0].vpos))`. `vpos=0` 인 문단(본문 첫 줄 등)은 종전과 동일(0). `vpos>0` 인 섹션-top/`다단나누기` band-top(제목 vpos=1984 등)은 그만큼 적용. +- 3쪽 본문영역 초과(#768 패턴 pi=94/95 vpos=0 겹침)는 **본 타스크 흡수** — Stage 3 에서 처리. 단 한글 PDF 페이지별 콘텐츠 대조로 "한글도 3쪽 초과인가" 먼저 확인 후 범위 확정. +- 제목 PUA 첫 글자(`\u{f53a}`) 폰트 폴백은 본 타스크 제외. + +## 단계 (5단계 — 원본과 동일, Stage 2/3 구체화) + +### Stage 2 — column-top 문단 `spacing_before` 의 LINE_SEG.vpos 클램프 +- `src/renderer/layout/paragraph_layout.rs:745-748` (`layout_composed_paragraph` 내): + - 현행: `if start_line == 0 && spacing_before > 0.0 && !is_column_top { y += spacing_before; }` + - 변경: `is_column_top` 일 때도 `start_line == 0 && spacing_before > 0.0` 이면 `y += min(spacing_before, vpos0_px)` 적용 (`vpos0_px = para.and_then(|p| p.line_segs.first()).map(|ls| hwpunit_to_px(ls.vpos as i32, self.dpi)).unwrap_or(0.0)`). `!is_column_top` 분기는 종전대로 `y += spacing_before` 전량. +- `src/renderer/height_measurer.rs` — column-top 문단 높이 계산이 `spacing_before` 전량을 포함하면, 위 클램프와 정합되도록 동일 클램프 적용(페이지네이션↔배치 비대칭 해소). 단 height_measurer 가 column-top 컨텍스트를 모르면, 보수적으로 "전량 포함" 유지 시 발생하는 영향(예약 과다 → 페이지 break 빨라짐)을 Stage 4 sweep 으로 점검 후 필요 시 수정. +- 검증: shortcut.hwp 7쪽 `export-svg` → 제목 top ≈ 83px, "커서 이동" 띠 ≈ 127px, 구분 칸 위·아래 여백 PDF ±6px 수렴. +- 산출: `mydocs/working/task_m100_853_stage2.md` + 소스 커밋. + +### Stage 3 — 3쪽 본문영역 초과 (#768 패턴) +- 한글 PDF 3쪽 콘텐츠 범위 대조 → 한글도 초과면 "정상"(수정 불요), rhwp 만 초과면 처리. +- pi=94(`<편집 화면 분할에서>`) / pi=95("화면 이동") 둘 다 vpos=0 겹침 원인 규명: `다단나누기` zone 분할 시 두 번째 문단의 vpos 가 0 으로 리셋되어 첫 문단 위에 겹침 → `src/renderer/layout.rs` zone 분할/`start_new_column_band` 부근에서 zone 내 문단 누적 y 가 vpos=0 을 잘못 해석하는 경로 수정. +- 누적 zone offset 이 body_bottom 초과 시 다음 페이지로 break 하는 가드 점검/보강. +- 산출: `mydocs/working/task_m100_853_stage3.md` (+ 소스 커밋 또는 불요 사유). + +### Stage 4 — 광역 회귀 검증 +- `cargo test`(svg_snapshot 8/8 포함) 전건 통과. `cargo clippy --lib -- -D warnings`(pre-existing 무관 오류 명시). +- 전 fixture SVG sweep → shortcut.hwp 외 byte-identical 또는 의도 변경만 입증. (`feedback_essential_fix_regression_risk` 정합 — 다단/단일 단/표분할 상호작용 회귀 점검.) +- shortcut.hwp 7쪽 SVG ↔ `pdf/basic/shortcut-2022.pdf`(+ 가능 시 `pdf-2020/`) 시각 정합 최종 확인. +- 산출: `mydocs/working/task_m100_853_stage4.md` + 커밋. + +### Stage 5 — 최종 결과보고서 +- `mydocs/report/task_m100_853_report.md` + 커밋. orders 미수정. + +## 회귀 가드 + +- column-top 클램프는 `vpos` 기록값을 상한으로만 쓰므로 "vpos 무시" 현행 대비 항상 ≥ 0, ≤ spacing_before — 과도 적용 위험 없음. +- 매 단계 `cargo test` 통과 필수, Stage 4 sweep 필수. +- vpos 해석 변경은 `respect_vpos_reset` 류 전면 변경과 무관(여기서는 column-top 첫 줄 한정 상한 클램프). diff --git a/mydocs/plans/task_m100_853_impl_v3.md b/mydocs/plans/task_m100_853_impl_v3.md new file mode 100644 index 000000000..7550b3f1a --- /dev/null +++ b/mydocs/plans/task_m100_853_impl_v3.md @@ -0,0 +1,30 @@ +# 구현 계획서 v3 (범위 확대) — Task #853 (M100) + +GitHub Issue: edwardkim/rhwp#853 · 브랜치: `local/task853` +선행: v1 `task_m100_853_impl.md`, v2 `task_m100_853_impl_v2.md` · Stage 1 진단 `task_m100_853_stage1.md` + +## 범위 (작업지시자 지시: "제목 + band 간격 + overflow 전부") + +shortcut.hwp 의 3개 결함을 모두 정정한다 — ① 섹션-top 제목 spacing_before 압축, ② `다단나누기` 구분 칸 band 위·아래 간격 압축, ③ 3쪽 본문영역 초과(#768 패턴). 본질 정정 영역(RFC #774) — 광역 회귀 + 한컴 정답지 검증 필수(`feedback_essential_fix_regression_risk`). + +## Stage 2 — 섹션-top 제목 spacing_before 클램프 (재적용, 완료) +- `src/renderer/layout/paragraph_layout.rs`: `is_column_top && para_index == 0` 일 때 `y += spacing_before.min(hwpunit_to_px(line_segs[0].vertical_pos).max(0.0))`. 페이지 break 후 column-top(`para_index>0`)은 종전대로 0. +- 효과: shortcut.hwp 제목 baseline 79.4 → 105.8 (top ≈ 83.8px ≈ 한컴 PDF 83.6px). svg_snapshot 2건(KTX p1, exam_kor p5)의 섹션-시작 문단도 LINE_SEG.vertical_pos 기준으로 재배치 → `UPDATE_GOLDEN` (한컴이 파일에 기록한 실제 렌더 위치와 정합하므로 개선). `cargo test --release` 전건 통과. +- 잔존: shortcut.hwp 7쪽 → 8쪽 (Stage 3 의 band 정정 후 재확인 — band 가 커지면 한컴처럼 7쪽 가능 여부 미정). + +## Stage 3 — `다단나누기` 구분 칸 band 위·아래 간격 + 3쪽 overflow +- **진단(Stage 1/2 에서 파악)**: 헤더 띠 문단(예: pi=36 "파일")의 LINE_SEG 는 line0=텍스트(lh=1200HU≈16px) + line1=표(lh=2332HU = 표 1766 + outer_margin 283×2 ≈ 31px) = 47px 인데, rhwp 는 표를 line0 에 놓고 텍스트 line0 을 흡수해 ≈27px → ~20px 부족(= stage5 "헤더↔본문 ~20px"). 또 zone 사이(제목 zone↔헤더 띠↔본문 zone)에 한컴이 두는 간격(~17px)이 rhwp 에는 0. 3쪽 단3 pi=94/95 vpos=0 겹침(#768 패턴). +- **방향(미확정 — 추가 진단 필요)**: (a) 헤더 띠 문단을 LINE_SEG 순서대로(text line0 → table line1) 렌더하도록 composer/`layout_table_item` 정정, (b) zone 전환 간격의 출처 규명(LINE_SEG.vertical_pos / `다단나누기` 후 첫 zone offset / 빈 continuation line) 후 정정, (c) 3쪽 overflow = (a)(b) 정정 후 자연 해소되는지 확인 + 안 되면 page break 가드 보강. +- 이 단계는 composer/table layout 변경으로 회귀 위험이 크므로, **Stage 3-1(추가 진단·설계 문서) → 승인 → Stage 3-2(구현)** 로 쪼갠다. 진단·설계는 `mydocs/tech/` 또는 본 working 문서에 정리. +- 산출: `task_m100_853_stage3.md` (+ 소스 커밋, 필요 시 RFC #774 일부 흡수). + +## Stage 4 — 광역 회귀 검증 +- `cargo test --release` 전건 + `cargo clippy --lib -- -D warnings`(pre-existing 무관 오류 명시). 전 fixture SVG sweep → shortcut.hwp/KTX/exam_kor 외 byte-identical 또는 의도 변경만. shortcut.hwp 7~8쪽 SVG ↔ `pdf/basic/shortcut-2022.pdf`(+ 가능 시 `pdf-2020/`) 시각 정합. 한컴 2010/2020 환경 차이 점검(`feedback_pdf_not_authoritative`). +- 산출: `task_m100_853_stage4.md` + 커밋. + +## Stage 5 — 최종 결과보고서 +- `mydocs/report/task_m100_853_report.md` 갱신(v2) + 커밋. orders 미수정. + +## 회귀 가드 +- Stage 2 클램프: `vertical_pos`(파일 기록값) 상한 → 항상 `0 ≤ applied ≤ spacing_before`. 과도 적용 불가. +- Stage 3: composer/table layout 변경 — 매 변경 후 `cargo test --release` 통과 필수, Stage 4 sweep 필수, 한컴 정답지 비교. diff --git a/mydocs/plans/task_m100_866.md b/mydocs/plans/task_m100_866.md new file mode 100644 index 000000000..b95f92442 --- /dev/null +++ b/mydocs/plans/task_m100_866.md @@ -0,0 +1,37 @@ +# 수행 계획서 — Task #866 (M100) + +GitHub Issue: edwardkim/rhwp#866 · 브랜치: `local/task866` (← `pr-task853` = upstream/devel `2bd50a3a` + PR #868 의 Task #853 정정) + +## 배경 + +Task #853(PR #868)이 shortcut.hwp 의 구분 칸 spacing 압축을 대부분 정정했으나, **`쪽나누기`로 시작하는 페이지(2쪽 "파일" 등)의 헤더 TAC 1×1 표 띠와 그 아래 본문 첫 줄 사이 ~28px gap** 이 잔존. pi=36 의 `다단나누기` ColumnDef = `1단, 간격=0mm` 이라 Task #853 Stage 3-3 의 ColumnDef-간격 가설(1쪽은 `간격=10mm` 으로 정합)로는 0px. 출처 미규명. + +측정(한컴 PDF `pdf/basic/shortcut-2022.pdf` 2쪽, `mutool draw -r 100` @96dpi): 헤더 띠 상단 +19.1px / 하단 +43.1px / 본문 첫 줄 ~+75px → 띠 하단↔본문 ~32px. rhwp(PR #868 적용): 헤더 띠 +19.8/+43.3px(정합), 본문 zone_y_offset = +47.1px → 띠 하단↔본문 ~4px. **~28px 부족.** + +## 후보 (분석 문서 `mydocs/tech/hancom_zone_paragraph_spacing.md` §5) + +1. TAC 표 `wrap=위아래(TopAndBottom)` 가 글자처럼 취급이면서도 위아래 어울림으로 표 아래 추가 예약 +2. 표 `쪽나눔=RowBreak(attr=0x04000006)` 의 0x04000000 비트 의미 +3. 1단 zone → 2단 배분(Distribute) zone 전환 시 한컴 고정 간격 + +## 위험 / 제약 + +- RFC #774 영역. 닫힌 PR #771/Issue #770(line0 흡수 ~16px 만 다룸 — Task #853 Stage 3-2 가 동일 효과), 닫힌 #773/#776 모두 이 ~28px 를 못 닫음. **macOS 환경 — 한컴 편집기 cross-check 불가**(`pdf/`(2022)·`pdf-2020/` PDF 측정으로 대체). +- composer/typeset 변경이라 광역 회귀 위험(`feedback_essential_fix_regression_risk`). 룰/휴리스틱 구분 필요(`feedback_rule_not_heuristic`). +- **#866 은 PR #868 에 의존** — 본 브랜치는 `pr-task853` 에서 분기(stacked). PR #868 머지 후 본 PR 은 갱신된 devel 로 rebase. + +## 진행 절차 + +1. **Stage 1 — PDF 측정 (코드 미수정)**: shortcut.hwp 2·3·4·5쪽 등 모든 `쪽나누기` 시작 페이지 및 같은-페이지 `다단나누기` zone 의 헤더 띠↔본문 거리를 `mutool draw -r 100` 픽셀 측정 → IR(`dump` / `dump-pages`)과 대조. ~28px 가 일정한지 / `쪽나누기` 한정인지 / 표 size·outer_margin·`쪽나눔` 비트와의 상관관계 파악. 후보 1~3 중 특정 또는 "측정만으로 미특정 → 보류" 판정. → `task_m100_866_stage1.md`. +2. (특정 시) **Stage 2 — 구현**: 규명된 근원에 한해 최소 수정(`typeset.rs::place_table_with_text` 또는 `process_multicolumn_break`) → `cargo test --release` + shortcut.hwp 7~8쪽 SVG↔PDF. +3. **Stage 3 — 광역 회귀**: `cargo test` 전건 + 전 fixture sweep + `pdf-2020/` 등 대조. +4. **Stage 4 — 최종 보고서**. (#867 — 페이지 수 7≠8 + 잔존 overflow 2건 — #866 해결 후 재평가; 본 타스크 범위는 #866 한정.) + +## 산출물 + +- `mydocs/working/task_m100_866_stage{N}.md`, `mydocs/report/task_m100_866_report.md`. 필요 시 `mydocs/tech/hancom_zone_paragraph_spacing.md` 갱신. + +## 승인 요청 + +1. 본 수행 계획 진행 승인. +2. 브랜치 base — `pr-task853`(권장, stacked) vs `upstream/devel`(PR #868 미반영 → 측정이 부정확) 중 확인. diff --git a/mydocs/plans/task_m100_866_impl.md b/mydocs/plans/task_m100_866_impl.md new file mode 100644 index 000000000..a7649728f --- /dev/null +++ b/mydocs/plans/task_m100_866_impl.md @@ -0,0 +1,27 @@ +# 구현 계획서 — Task #866 (M100) + +GitHub Issue: edwardkim/rhwp#866 · 브랜치: `local/task866` (← `pr-task853`) · 수행 계획서: `task_m100_866.md` + +## 단계 (4단계) + +### Stage 1 — PDF 측정 + IR 대조 (코드 미수정) +- 빌드: `pr-task853` 상태 그대로(PR #868 적용). `cargo build --release` (필요 시). +- shortcut.hwp 의 `쪽나누기` 시작 페이지(2쪽 "파일", 그리고 같은-페이지 `다단나누기` zone 들)에서 헤더 TAC 띠 상단/하단/본문 첫 줄 y 를 `mutool draw -r 100` 픽셀 측정(@96dpi 환산) → `dump`/`dump-pages` 의 IR(line_segs vpos/lh/ls, 표 size·outer_margin·`쪽나눔` attr, ColumnDef 간격·column_type)과 대조. +- 검증 항목: ① ~28px gap 이 모든 `쪽나누기` 헤더 띠에서 일정한가 / ② `다단나누기`(쪽나누기 아닌)로 시작한 헤더 띠에서도 나타나는가 / ③ 표 size·outer_margin·`쪽나눔=RowBreak(0x04...)` 비트와 상관관계 / ④ 후속 본문 zone 의 첫 LINE_SEG vpos 가 0 인가(= 본문이 zone 상단에 붙는가) → 28px 가 zone 진입 offset 인지 본문 paragraph 의 leading 인지. +- 산출: `task_m100_866_stage1.md` — 측정표 + 후보 1~3 중 특정(있으면) + 정정 위치/방법 또는 "미특정 → 보류" 판정. (보류 시 Stage 2~3 생략하고 Stage 4 보고서로 마감.) + +### Stage 2 — 구현 (Stage 1 에서 근원 특정 시) +- 규명된 경로(`typeset.rs::place_table_with_text` 또는 `process_multicolumn_break`)에 최소 수정. 룰로 명세 가능하면 단일 룰, 아니면 휴리스틱 vs 룰 자문 후 적용. +- 검증: shortcut.hwp 7~8쪽 SVG ↔ `pdf/basic/shortcut-2022.pdf` — 헤더 띠↔본문 거리 PDF ±6px 수렴. +- 산출: `task_m100_866_stage2.md` + 소스 커밋. + +### Stage 3 — 광역 회귀 +- `cargo test --release` 전건 + `cargo clippy --lib -- -D warnings`(pre-existing 무관 오류 명시). 전 fixture SVG sweep → shortcut.hwp 외 byte-identical 또는 의도 변경만. shortcut.hwp ↔ `pdf/`(2022) (+ 가능 시 `pdf-2020/`) 시각 정합. +- 산출: `task_m100_866_stage3.md` (+ 커밋). + +### Stage 4 — 최종 보고서 +- `mydocs/report/task_m100_866_report.md` + 커밋. 필요 시 `mydocs/tech/hancom_zone_paragraph_spacing.md` 갱신. (#867 은 #866 결과 보고 별도 재평가 — 본 타스크 범위 외.) + +## 회귀 가드 +- 매 단계 `cargo test --release` 통과 필수. Stage 3 sweep 필수. 한컴 정답지 등급(`pdf/` 2022 1차, `pdf-2020/` 보조; `pdf-2010/` 등급 미달). +- 측정만으로 출처 미특정 시 강행하지 않고 보류(`feedback_essential_fix_regression_risk`). diff --git a/mydocs/report/task_m100_853_report.md b/mydocs/report/task_m100_853_report.md new file mode 100644 index 000000000..d312fd4dc --- /dev/null +++ b/mydocs/report/task_m100_853_report.md @@ -0,0 +1,48 @@ +# 최종 결과 보고서 v2 — Task #853 (M100) + +대상: `samples/basic/shortcut.hwp` ↔ 한컴 PDF `pdf/basic/shortcut-2022.pdf` — (증상1) 모든 구분 칸 위·아래 줄 간격 압축, (증상2) 일부 페이지 본문영역 초과 렌더링. +GitHub Issue: edwardkim/rhwp#853 · 브랜치: `local/task853` (← upstream/devel `2bd50a3a`) + +> 본 보고서는 v2. v1(`13cd40bb` 시점)은 옵션 B(소스 변경 0, RFC 선행 전환)였으나, 작업지시자 지시로 범위를 "제목 + band 간격 + overflow 전부"로 확대해 Stage 2/3 를 정정 구현했다. + +## 결과 요약 + +| # | 결함 | 결과 | +|---|------|------| +| Stage 2 | 섹션-top 제목 spacing_before 압축 | ✅ 수정 — `paragraph_layout.rs` 에서 `is_column_top && para_index==0` 시 `spacing_before` 를 LINE_SEG.vertical_pos 로 클램프. 제목 baseline 79.4→105.8 (top≈83.8px ≈ 한컴 PDF 83.6px). | +| Stage 3-2 | 헤더 띠(TAC 표) line0 텍스트 흡수 | ✅ 수정 — `typeset.rs::place_table_with_text` 에서 전폭 TAC 표가 자기 줄에 놓인 경우 표 줄 높이와 일치하는 LINE_SEG 인덱스로 표 앞 텍스트 줄(line0)을 표보다 먼저 배치. PUA 필러 케이스(복학원서.hwp pi=16)는 `is_alphanumeric()` 로 제외. 2쪽 "파일" 헤더 띠 상단 +3.8→+19.8px (한컴 PDF +19.1px ✓), 하단 +27.3→+43.3px (PDF +43.1px ✓). | +| Stage 3-3 | 1단 ColumnDef `간격` 의 zone 진입 세로 간격 미반영 | ✅ 수정 — `typeset.rs` zone 전환(`process_multicolumn_break` / `force_new_page`+diff_col_def)에서 (이전 zone 디자인 spacing /2)+(새 zone 디자인 spacing /2) 를 `zone_y_offset` 에 더함. '디자인 spacing'=1단 ColumnDef 의 `간격`(가로 무의미), 다단은 0. 1쪽 헤더 띠 zone_y_offset 69.1→88.0 (한컴 PDF +87.6px ✓), 본문 zone 100.2→138.0 (PDF +137.9px ✓). | +| 잔존 a | 2쪽(`쪽나누기`로 시작) 헤더 띠↔본문 ~28px gap | ⏸ 미수정 — pi=36 ColumnDef `간격=0mm` 이라 Stage 3-3 가설로 0px, 출처 미규명. 후보(분석 문서 §5): TAC `wrap=위아래` 추가 예약 / `쪽나눔=RowBreak(0x04000006)` / 1단→2단 전환 고정 간격. 한컴 편집기(Windows) cross-check + 추가 샘플 측정 필요. | +| Stage 3-3b/c | 다단 zone 누적 시 잔여 콘텐츠가 본문영역 초과 | ✅ 보강 — ① `process_multicolumn_break`: 새 zone 시작 여유 < 헤더 띠 1개 높이(~56px)면 `push_new_page`. ② vpos-reset 검출(Distribute 다단): 직전 문단 `vpos+line_height` 기준으로 비교 → 1줄짜리 컬럼(prev vpos=0, curr vpos=0)도 컬럼 전환 인식(shortcut.hwp 스타일/속성 섹션). LAYOUT_OVERFLOW 25→13→2. | +| 잔존 b | shortcut.hwp 페이지 수 7≠8 + 잔존 LAYOUT_OVERFLOW 2건 | ⏸ 미수정 — 잔존 2건(pi=149/150, 4쪽 col 0/1, 6.7px)은 zone 이 본문 하단 직전에서 시작하는 경계 케이스. 페이지 수 7≠8 은 §a(2쪽 ~28px gap) 미해결로 zone 누적이 한컴과 어긋난 결과. | + +`cargo test --release` 전건 통과(34 test suites). svg_snapshot 8/8 — golden 2건(`issue-267/ktx-toc-page`, `issue-617/exam-kor-page5`) 갱신(Stage 2 — 섹션-시작 문단이 LINE_SEG.vertical_pos 기준으로 재배치, 한컴 기록값 정합). 다른 문서 회귀 없음. + +## 변경 내역 + +### Stage 2 — `src/renderer/layout/paragraph_layout.rs::layout_composed_paragraph` +- `is_column_top` 시 `spacing_before` 를 통째 드롭하던 것을, `para_index == 0`(섹션 첫 문단)인 경우 `y += spacing_before.min(hwpunit_to_px(line_segs[0].vertical_pos))` 로 변경. 페이지 break 후 column-top(`para_index>0`)은 종전대로 0. + +### Stage 3-2 — `src/renderer/typeset.rs::place_table_with_text` +- `pre_table_end_line` 계산에 분기: `table.common.treat_as_char && total_lines > 1 && para.text.chars().any(is_alphanumeric)` 이면, 표 줄 높이(표 본체 + outer_margin top/bottom)와 일치하는 LINE_SEG 인덱스를 사용 → 그 앞 줄을 `PageItem::PartialParagraph{0..pre}` 로 표보다 먼저 emit. `tac_wrap_split` 플래그로 높이 이중계산 방지(`table_total_height` 만 누적) + post-text 시작을 `pre+1` 로. + +### Stage 3-3 — `src/renderer/typeset.rs` (`TypesetState`, `process_multicolumn_break`, `paginate_section`, `force_new_page` 경로) +- `current_zone_design_spacing_px` 필드 추가, `column_def_design_spacing_px(cd, dpi)` 헬퍼(1단이면 `간격`, 다단이면 0). +- zone 전환 시 `zone_y_offset += (이전 zone 디자인 spacing /2) + (새 zone 디자인 spacing /2)`. 새 페이지 첫 zone 은 새 zone /2 만(이전 zone 은 이전 페이지). + +## 미수정 — 잔존 항목 (신규 후속 이슈로 등록) + +분석 문서 `mydocs/tech/hancom_zone_paragraph_spacing.md` (RFC #774 후속) 에 정리: +1. **#866** — 쪽나누기로 시작하는 페이지의 헤더 TAC 띠↔본문 ~28px gap 미재현. pi=36 ColumnDef `간격=0mm` 이라 Stage 3-3 가설로 미설명. 닫힌 PR #771/Issue #770/RFC #774 도 못 닫은 본질(line0 흡수 ~16px 만 다룸 — Stage 3-2 가 동일 효과). 한컴 정답지 cross-check / 다른 헤더 띠 PDF 측정 필요. +2. **#867** — 페이지 수 7≠8 + 잔존 LAYOUT_OVERFLOW 2건(pi=149/150, 4쪽, 6.7px). #866 에 종속(zone 누적이 한컴과 어긋난 결과). 별개로 vpos=0 연속 문단(#768 패턴)이 비-배분 다단에 잔존 가능. +3. 부수: 제목 PUA 첫 글자 `\u{f53a}` — SVG 에 출력되며 폰트 폴백 영역(본 타스크 무관). + +본 타스크는 위 잔존을 #866/#867 로 분리하고 종료. Issue #853 close. + +## 검증 + +- `cargo build --release` 성공. `cargo test --release` 34 suites ok / 0 failed. svg_snapshot 8/8. +- shortcut.hwp 7~8쪽 SVG ↔ `pdf/basic/shortcut-2022.pdf` 시각 비교(`mutool draw -r 100` 픽셀 측정): 1쪽 제목·헤더 띠·본문 첫 줄, 2쪽 헤더 띠 상/하단 모두 한컴 PDF 와 ±1px 수렴. 2쪽 본문은 ~28px 잔존. + +## 커밋 +`38042d05`(계획서) → `6faf8250`(Stage 1) → `b65c1624`(impl v2) → `13cd40bb`(v1 보고/일시 revert) → `f0d34713`(Stage 2 재적용) → `bd9d5148`/`0fca34ed`(Stage 3-1 조사·분석) → `c765e36b`(3-2 1차 실패) → `1f7328b2`(3-2 성공) → `4ae34c55`(3-3 조사) → `d011d43d`(3-3 구현) → 본 보고서. → `b74967bf`(Stage 3-3 page break 보강). diff --git a/mydocs/report/task_m100_866_report.md b/mydocs/report/task_m100_866_report.md new file mode 100644 index 000000000..22a3b13c6 --- /dev/null +++ b/mydocs/report/task_m100_866_report.md @@ -0,0 +1,38 @@ +# 최종 결과 보고서 — Task #866 (M100) + +대상: shortcut.hwp 의 `쪽나누기`로 시작하는 페이지(2쪽 "파일" 등)에서 헤더 TAC 표 띠↔본문 사이 ~28px gap 미재현. (Task #853 의 잔존 #866; #847 의 잔여; 닫힌 PR #771/#770/#773/#776, RFC #774 가 못 닫은 본질.) +GitHub Issue: edwardkim/rhwp#866 · 브랜치: `local/task866` (← `pr-task853` = upstream/devel + PR #868) + +## 결과 요약 + +| Stage | 내용 | 결과 | +|---|---|---| +| 1 | PDF 측정 (코드 미수정) | 헤더 띠 하단↔본문 gap = 2쪽 +31.7px, 3쪽 +33.6px (일정). ≈ TAC 표 band 의 line height(~31px). | +| 2 | 가설 정립 | 한컴은 `wrap=위아래` 글자처럼-취급 표(헤더 띠) 아래에 표 band 높이만큼 추가 여백. ColumnDef 간격>0 인 헤더 띠(1쪽)는 그 간격이 이미 여백이라 제외. | +| 3 | 구현 | ① `typeset.rs::process_multicolumn_break`: 직전 zone 마지막 paragraph 가 `wrap=위아래` 글자처럼-취급 표 & 1단 ColumnDef 간격=0 → `vpos_zone_height` 에 표 band 높이 추가. zone 가드 `3→4*one_line`. ② `layout.rs::build_columns`: zone-간 세로 여백(ColumnDef 간격/2 + 표 band)을 layout 의 `prev_zone_y_end` 누적에도 미러 — 종전엔 pagination 메타데이터(`current_zone_y_offset`)에만 반영되어 SVG 렌더에 미적용이었음. | + +### 핵심 발견 +PR #868(Task #853) Stage 3-3(1단 ColumnDef `간격` → zone 진입 세로 간격)은 pagination 의 `current_zone_y_offset`(메타데이터)만 갱신했고 layout 의 zone 스태킹(`build_columns` 의 `prev_zone_y_end` 누적)에는 미반영이라 SVG 렌더에 적용되지 않았었다(= PR #868 의 "1쪽 정합" 표기가 실제론 메타데이터만). Task #866 Stage 3 의 layout 미러로 비로소 1쪽도 시각 정합됨. + +## 시각 정합 (shortcut.hwp ↔ `pdf/basic/shortcut-2022.pdf`, SVG baseline ↔ PDF baseline 추정) + +| | 변경 전 | 변경 후 | 한컴 PDF | 평가 | +|---|---|---|---|---| +| 1쪽 본문 첫 줄 baseline (into body) | ~+111px | **+149.3px** | ~+148px | ✓ 정합 | +| 2쪽 본문 첫 줄 baseline | ~+58px | **+89.5px** | ~+85px | ~4.5px 초과 (종전 ~28px 부족) | +| 3쪽 본문 첫 줄 baseline | ~+71px | **+102.3px** | ~+93px | ~9px 초과 (종전 ~24px 부족) | + +→ 사용자 주 증상("모든 구분 칸 위·아래 줄 간격 좁음", 특히 헤더 띠↔본문 ~28px 부족)이 **전 페이지에서 해소**(잔여 ~4~9px 초과). + +## 검증 + +- `cargo test --release` 34 test suites 전건 통과. svg_snapshot 8/8 — golden 무변경. +- shortcut.hwp 8쪽 SVG ↔ `pdf/basic/shortcut-2022.pdf` 픽셀 측정으로 1·2·3쪽 본문 위치 확인. + +## 잔존 (미수정) + +1. `LAYOUT_OVERFLOW` 4건(4쪽 2단 본문 zone, pi=143~147) — 헤더 띠가 ~31px 커진 만큼 후속 zone 들이 밀려 본문 하단 초과. `process_multicolumn_break` 가드가 새 zone 의 *시작* 여유만 보므로 시작은 들어가나 zone 콘텐츠가 넘치는 케이스는 못 잡음. 가드를 콘텐츠 높이 추정 기반으로 보강 필요 → **#867** 영역(페이지 수 7≠8 포함). (원래 25 → PR #868 0 → Task #866 4.) +2. 2·3쪽 ~4~9px 초과 — tac_band(`table.height + om_top + om_bot`)가 한컴 실측 gap(~28~33px)보다 미세하게 큼. 정밀화는 한컴 편집기 cross-check 필요(macOS 환경 한계). + +## 커밋 (브랜치 `local/task866`, PR #868 위에 stacked) +PR #868(`91e585e3`) → 수행 계획서(`79aa64f7`) → 구현 계획서(`a72daadb`) → Stage 1 측정(`e5d579c6`) → Stage 2 분석(`adbfcfa0`) → Stage 3 구현(`8c87cbf7`) → 본 보고서. diff --git a/mydocs/tech/hancom_zone_paragraph_spacing.md b/mydocs/tech/hancom_zone_paragraph_spacing.md new file mode 100644 index 000000000..b947db58a --- /dev/null +++ b/mydocs/tech/hancom_zone_paragraph_spacing.md @@ -0,0 +1,74 @@ +# 한컴 paragraph/zone 수직 spacing 모델 (shortcut.hwp 정합) — RFC #774 후속 분석 + +작성 배경: Task #853 Stage 3-1. 닫힌 RFC [#774](https://github.com/edwardkim/rhwp/issues/774)("한컴 PDF paragraph spacing 알고리즘 정밀 분석")의 후속. shortcut.hwp ↔ `pdf/basic/shortcut-2022.pdf` 측정 + IR 구조 기반. + +## 1. LINE_SEG.vertical_pos 의미 (확인됨) + +LINE_SEG 의 `vertical_pos` = **zone(단/페이지 흐름 구간) 상단 기준 누적 절대값** = Σ(lh_i + ls_i) (선행 줄들의 line_height + line_spacing). + +근거: 본문 행 pi=37 `vpos=0, lh=1000, ls=500` → pi=38 `vpos=1500` (= 1000+500). pi=39 `vpos=3000`. 즉 한 zone 안에서 문단들의 첫 줄 vpos 는 누적된다. + +## 2. 섹션/페이지 첫 문단 — spacing_before 클램프 (Stage 2 에서 정정 완료) + +`para_index == 0` 이면서 column-top 인 문단(예: 제목 pi=0)은 한컴이 `spacing_before` 를 적용하되 **그 문단 첫 LINE_SEG.vertical_pos 로 상한 클램프**한다. + +- 제목 pi=0: `PS spacing_before=3968 HU (52.9px)`, `LINE_SEG[0].vertical_pos=1984 HU (26.45px)` → 적용값 = min = 1984 HU = 26.45px. 한컴 PDF 제목 텍스트 top = body_top(56.7px) + 26.9px ≈ 83.6px ≈ vertical_pos. ✓ +- rhwp 정정: `paragraph_layout.rs` 에 `is_column_top && para_index==0 → y += spacing_before.min(hwpunit_to_px(line_segs[0].vertical_pos))` 적용(커밋 `f0d34713`). `height_measurer`(이미 vertical_pos 반영)와 정합. + +## 3. `다단나누기`(ColumnDef) zone 진입 top spacing — **미규명, 정정 대상** + +shortcut.hwp 의 각 구분 칸 섹션은 `다단나누기`(ColumnDef control)로 새 zone 을 연다. ColumnDef 의 `간격`(column spacing) 필드 값: + +| 문단 | ColumnDef | `간격` | +|------|-----------|--------| +| pi=1 (1쪽 "커서 이동" 헤더 띠) | 1단, 일반 | **10.0mm (2835 HU = 37.8px)** | +| pi=36 (2쪽 "파일" 헤더 띠) | 1단, 일반 | 0.0mm | +| pi=2 / pi=37 (본문, 2단 배분) | 2단, 배분 | 1.0mm (283 HU) | + +- RFC #774 가설 B 검증 결과: ColumnDef.spacing 이 rhwp 의 `zone_y_offset` 에 반영 안 됨. +- 1단 ColumnDef 의 `간격`(원래는 단 사이 가로 간격이나 1단이라 가로 간격 무의미)이 **세로 zone 진입 간격으로 해석**되는지 확인 필요. 1쪽 "커서 이동" 띠 앞 ~38px 디자인 여백의 출처 후보. 2쪽 "파일" 띠는 `간격=0` 이라 이 항목으론 설명 안 됨 → 2쪽 deficit 은 §4 가 주원인. + +## 4. 헤더 띠 문단(TAC 표 단독 줄) — line0 텍스트 흡수, **정정 대상** + +헤더 띠 문단의 IR 구조가 1쪽(pi=1)과 2쪽 이후(pi=36)가 다르다: + +| 문단 | text_len | LINE_SEG | +|------|----------|----------| +| pi=1 (1쪽 "커서 이동") | 0 (빈 문단) | `ls[0]: vpos=0, lh=2332` (= 표 1766 + outer_margin 283×2). 줄 1개. | +| pi=36 (2쪽 "파일") | 2 ("파일") | `ls[0]: vpos=0, lh=1200` (텍스트 줄, 16px) + `ls[1]: vpos=1200, lh=2332` (표 줄, 31px). 줄 2개. | + +- pi=36 류: 한컴은 line0(텍스트 "파일", 16px) → line1(표, 31px) 순으로 배치(총 47px). **rhwp 는 표를 line0 에 놓고 텍스트 line0 을 흡수해 ~27px** → ~16~20px 부족. (표 셀 안에도 "파일" 이 있어 PDF 상 띠 1개만 보이지만, 문단 텍스트 "파일"(line0)은 띠 위 16px 줄에 별도로 흐른다.) +- pi=1 류(빈 문단): line0 = 표 줄 1개(2332 HU = 31px). rhwp 정합. → 1쪽 헤더 띠 자체 높이는 OK, 1쪽 deficit 은 §3. + +## 5. 측정 종합 (shortcut.hwp 2쪽, `mutool draw -r 100` → @96dpi 환산) + +| 요소 | 한컴 PDF (body_top 기준) | rhwp | 차이 | +|------|------------------|------|------| +| "파일" 헤더 띠 상단 | +19.1px | +3.8px | rhwp ~15px 높음 (= line0 텍스트 줄 흡수) | +| 헤더 띠 하단 | +43.1px | +27.3px | ~16px | +| 본문 첫 줄 "새 문서" 상단 | ~+75px | ~+29px | rhwp **~46px 높음** | +| 띠 하단 ↔ 본문 사이 | ~32px | ~2px | ~30px 부족 | + +- ~46px = ~15px(§4 line0 흡수) + ~30px(띠↔본문 gap). 띠↔본문 ~30px gap 의 출처: pi=36 zone 은 line0(16px)+line1(47px) = 47px 인데(즉 body zone 은 body_top+47px ≈ +47px 에서 시작해야 함) PDF 는 +75px → ~28px 미설명. 후보: ① 1단 zone → 2단 zone 전환 시 한컴 고정 간격, ② TAC `wrap=위아래`(TopAndBottom)가 글자처럼 취급이면서도 위아래 어울림으로 추가 예약, ③ 표 `쪽나눔=RowBreak` 처리, ④ 본문 첫 문단(pi=37)의 추가 leading. **추가 측정 필요** (3쪽 이후 다른 띠들의 PDF↔IR 대조로 패턴 확인). + +## 6. 3쪽 본문영역 초과 + +3쪽 단3 `<편집 화면 분할에서>`(pi=94)·"화면 이동"(pi=95) 둘 다 `vpos=0` 겹침 — 닫힌 #768 패턴. §3/§4/§5 정정으로 zone 누적이 정확해지면 자연 해소될 가능성. 안 되면 page break 가드(누적 offset > body_bottom 시 다음 페이지) 보강. + +## 7. 구현 계획 (Stage 3-2) + +순서대로, 각 단계마다 `cargo test --release` + shortcut.hwp 7~8쪽 SVG↔PDF 확인: + +1. **헤더 띠 line0 텍스트 렌더** (`composer.rs` / `layout/table_partial.rs` / `layout_table_item`): 헤더 띠 문단을 LINE_SEG 순서(text line0 → table line1)대로 배치. rhwp 가 표를 line0 에 올리는 경로 차단. 영향: pi=36 류 전부 +16px. +2. **추가 측정 → 띠↔본문 ~28px gap 규칙 확정** (3쪽 이후 띠들 PDF↔IR 대조). 확정되면 해당 경로(zone 전환 간격 / TAC wrap 예약 / RowBreak)에 적용. +3. **ColumnDef.spacing → 1단 zone 진입 top 간격** (`build_columns` `zone_y_offset`): 1쪽 "커서 이동" 띠 앞 ~38px. 단, 회귀 위험 — 다른 문서의 1단 ColumnDef 사용처(전 fixture sweep)로 영향 확인. 한컴 정답지로 1단 ColumnDef `간격`의 세로 적용 여부 재확인 필수. +4. 3쪽 overflow 재확인 → page break 가드 필요 시 보강. +5. 광역 회귀(`cargo test`, 전 fixture sweep, 한컴 2010/2020 정답지). + +회귀 위험: §1·§2 = 낮음(완료). §4(composer 변경) = 중간~높음. §3(zone 진입 간격) = 높음 — `feedback_essential_fix_regression_risk` 정합, 전 fixture sweep + 한컴 정답지 필수. + +## 미해결 / 추가 작업 + +- 띠↔본문 ~28px gap 의 정확한 출처 (§5 후보 ①~④) — 3쪽 이후 띠 추가 측정 필요. +- 1단 ColumnDef `간격`의 세로 적용 — 한컴 편집기(Windows) 또는 다른 샘플 cross-check. +- 본 모델은 shortcut.hwp 의 `다단나누기`+TAC 표 헤더 띠 패턴 한정. 다른 zone/spacing 패턴은 별도. diff --git a/mydocs/working/task_m100_853_stage1.md b/mydocs/working/task_m100_853_stage1.md new file mode 100644 index 000000000..f5bfc1aa6 --- /dev/null +++ b/mydocs/working/task_m100_853_stage1.md @@ -0,0 +1,71 @@ +# Stage 1 완료 보고서 — Task #853 (M100) — 진단 + +GitHub Issue: edwardkim/rhwp#853 · 브랜치: `local/task853` · 코드 미수정 (진단 전용) + +## 1. 관측 데이터 (shortcut.hwp 1쪽) + +`export-svg` 출력 (`output/svg/sc842/shortcut_001.svg`) ↔ `pdf/basic/shortcut-2022.pdf` (stage5 `mydocs/working/task_m100_842_stage5.md` 의 pdftotext -bbox 측정): + +| 요소 | rhwp SVG | PDF (한글 2022) | 차이 | +|------|----------|-----------------|------| +| 본문영역 상단 | y=56.7 | 15mm ≈ 56.7 | 0 | +| 제목 텍스트 (baseline / top) | baseline 79.4 / top ≈ 58 | top ≈ 83.6 | **rhwp ~25px 높음** | +| "커서 이동" 헤더 띠 rect | y=103.1 ~ 126.7 (h=23.5) | 텍스트 144~160 → 띠 ~127~150 추정 | **rhwp ~25px 높음** | +| 첫 본문행 "빈칸 삽입" | y ≈ 130.5 (column rect 상단) | 텍스트 top ≈ 194.8 | **rhwp ~64px 높음** | +| 본문 행 pitch | ~20px (vpos 1500 HU) | ~20px (15pt) | 동일 | + +→ 콘텐츠가 위로 갈수록 누적 압축: 제목 ~25px + 헤더 띠 ~25px(누적) + 헤더↔본문 ~+39px(누적 64px). 본문 행 pitch 자체는 정상. + +## 2. 근원 규명 — 확인됨 + +### (A) `다단나누기` column-band-top / 섹션-top 문단에서 `spacing_before` 가 통째로 버려짐 ★ 주 원인 + +`src/renderer/layout/paragraph_layout.rs:745-748`: +```rust +// 단/페이지의 맨 처음 문단은 spacing_before 적용하지 않음 +let is_column_top = (y - col_area.y).abs() < 1.0; +if start_line == 0 && spacing_before > 0.0 && !is_column_top { + y += spacing_before; +} +``` +- 제목 문단 0.0: `PS before=3968 HU` (= 52.9px), `LINE_SEG vpos=1984 HU` (= 26.45px). 제목은 섹션 0 의 첫 문단 → `y == col_area.y` → `is_column_top=true` → **`before` 52.9px 가 0 으로 버려짐**. 그래서 제목 baseline 이 `body_top + ascent ≈ 79.4` 에 놓임. +- 한글 2022 PDF 는 제목 텍스트 top 을 `body_top + 26.9px ≈ 83.6` 에 놓음 — 이는 `LINE_SEG vpos=1984 HU (26.45px)` 와 정확히 일치. 즉 **한글은 column/섹션 top 문단에서도 LINE_SEG.vpos(= `before/2` 인 경우)를 그대로 존중**하는데, rhwp 는 `is_column_top` 예외로 통째 버린다. +- shortcut.hwp 의 각 섹션 헤더(`커서 이동`·`지우기`·`파일`·…)는 `다단나누기` 컨트롤로 새 column band 를 시작하고, 그 band 의 첫 문단(= TAC 표 anchor 문단 또는 본문 첫 줄)이 매번 `is_column_top` 이 되어 동일하게 `spacing_before` 가 사라진다 → 모든 구분 칸 위·아래 간격이 ~20px 씩 부족 (사용자 보고 증상 1). + +### (B) 페이지네이션(`height_measurer`)과 배치(`paragraph_layout`)의 `spacing_before` 비대칭 + +`src/renderer/height_measurer.rs:341`: +```rust +let total_height = (spacing_before + lines_total + spacing_after - clickhere_adjustment).max(0.0); +``` +- `height_measurer` 는 `is_column_top` 가드가 없어 column-top 문단도 `spacing_before` 를 **항상** 높이에 포함. 반면 `paragraph_layout` 은 버린다 → 페이지네이터가 예약한 높이와 실제 렌더 높이가 어긋남. + +## 3. 페이지 영역 초과 (증상 2) — 부분 규명 + +- 3쪽: 단0~단18(19개 zone) 누적, 마지막 zone(단17/18) `zone_y_offset = 720.2px` 인데 body_area 높이는 701.7px → 콘텐츠 y ≈ 766 > body_bottom 758.4 (SVG max y 측정). 1·2·4·5·7쪽은 본문영역 내, 6쪽 752.6px. +- (B) 의 비대칭이 직접 원인은 아닐 가능성(렌더가 페이지네이션보다 *위로* 어긋나면 콘텐츠가 더 일찍 끝남) — 3쪽 초과는 **별개 결함**으로 보임: + - 3쪽 단3: `<편집 화면 분할에서>`(pi=94, vpos=0)와 "화면 이동"(pi=95, vpos=0)이 **둘 다 vpos=0** 으로 겹침 → 닫힌 **#768**(다단 zone 분할 행 밀림) 패턴. 이 zone 높이 오산이 3쪽 누적에 기여. + - 페이지네이터가 누적 zone offset 이 body_bottom 을 넘는데도 다음 페이지로 break 하지 않는 경로(TAC 표 띠 + 다단 zone 조합) 의심 — 단, 한글 PDF 3쪽이 동일 콘텐츠를 모두 담는지(= 한글도 본문영역 초과 vs rhwp 가 과적재) Stage 2 에서 PDF 페이지별 콘텐츠 대조로 확정 필요. + +## 4. 영향 코드 경로 + +| 경로 | 역할 | +|------|------| +| `src/renderer/layout/paragraph_layout.rs:745-748` | `is_column_top` 예외 — 주 수정 후보 | +| `src/renderer/height_measurer.rs:341` 외 | column-top 시 `spacing_before` 포함 여부 — `paragraph_layout` 과 정합 필요 | +| `src/renderer/layout.rs` zone 배치(`start_new_column_band` 부근), 페이지 break 판정 | 3쪽 초과 — 누적 offset > body_bottom 시 break, #768 패턴 | +| LINE_SEG `vpos`/`lh`/`bl` 해석 (`src/document_core/` / paragraph_layout 줄 루프) | 대안: column-top 에서 LINE_SEG.vpos 를 첫 줄 위치로 존중 | + +## 5. Stage 2 방향 (옵션) + +1. **옵션 A — `is_column_top` 예외 범위 축소**: 페이지 break 로 *연속*된 column-top 에서만 `spacing_before` 드롭, 섹션-top·`다단나누기` band-top 에서는 적용. 컨텍스트(연속 여부)를 caller 에서 전달 필요. + `height_measurer` 정합. +2. **옵션 B — column-top 에서 LINE_SEG.vpos 존중**: 파일에 기록된 vpos(= 한글이 실제 렌더한 위치)를 첫 줄 top 으로 사용. 가장 충실하나 `respect_vpos` 류 — `feedback_essential_fix_regression_risk` 경고(다단/단일 단/표분할 상호작용 회귀 위험) 적용, 광역 sweep 필수. +3. **옵션 C — column-top 에서 `spacing_before/2` 적용**: vpos=1984 = before/2 관찰 기반. shortcut.hwp 한정으로는 맞지만 일반 룰 근거 불충분 — `feedback_rule_not_heuristic` 위배 소지. 비권장. + +→ **권장: 옵션 A** (예외 범위 축소 + height_measurer 정합) 를 1차 시도, 안 맞으면 옵션 B 검토. 3쪽 초과(#768 패턴)는 Stage 3 에서 별도 처리하되 한글 PDF 페이지별 콘텐츠 대조로 "한글도 초과인가" 먼저 확인. + +## 6. 결정 사항 (작업지시자 승인 요청) + +1. Stage 2 옵션 — A(권장) / B / C 중 선택. +2. 3쪽 초과(#768 패턴) — 본 타스크 흡수 vs #768 재오픈 분리. (Stage 1 결과: rhwp 측 zone 높이 오산이 근원으로 보이나 `다단나누기` 영역이라 #768 과 동일 근원일 가능성 — 본 타스크 흡수 권장.) +3. 부수: 제목 PUA 첫 글자 `\u{f53a}` — SVG 에는 출력되나 폰트 미지원 시 미표시(폰트 폴백 영역). 본 타스크 범위 제외 권장. diff --git a/mydocs/working/task_m100_853_stage2.md b/mydocs/working/task_m100_853_stage2.md new file mode 100644 index 000000000..06d3174b2 --- /dev/null +++ b/mydocs/working/task_m100_853_stage2.md @@ -0,0 +1,40 @@ +# Stage 2 진행 보고서 — Task #853 (M100) — 부분 결과 + 판단 요청 + +GitHub Issue: edwardkim/rhwp#853 · 브랜치: `local/task853` · 상태: **소스 변경 1건 적용(미커밋), 판단 요청** + +## 적용한 변경 (미커밋) + +`src/renderer/layout/paragraph_layout.rs::layout_composed_paragraph` (현행 745-748 부근): +- 현행: column-top 문단(`is_column_top`)은 `spacing_before` 를 통째 드롭(`!is_column_top` 일 때만 적용). +- 변경: column-top 이면서 **섹션의 첫 문단(`para_index == 0`)** 인 경우, `spacing_before` 를 그 문단 첫 LINE_SEG 의 `vertical_pos` 로 상한 클램프해 적용 (`y += spacing_before.min(hwpunit_to_px(line_segs[0].vertical_pos).max(0.0))`). 페이지 break 후 이어진 column-top(`para_index > 0`)은 종전대로 0. + +## 결과 + +### ✅ 효과 — shortcut.hwp 제목 위치 정합 +- 제목 "글 2010 단축키 일람표" baseline y=79.4 → **105.8** (+26.4px). top ≈ 83.8px ≈ 한컴 PDF top 83.6px (`pdf/basic/shortcut-2022.pdf`, stage5 측정). 한컴이 적용하는 `LINE_SEG.vertical_pos = 1984 HU (26.45px)` 와 일치. +- `height_measurer` 는 이미 제목 높이에 26.5px(=vpos)를 포함하고 있었으나(`dump-pages` 단0 `sb=26.5`), `paragraph_layout` 만 0 으로 드롭해 비대칭이었음 → 이번 변경으로 페이지네이션↔배치 정합. + +### ⚠ 문제 1 — shortcut.hwp 페이지 수 7 → 8 (한컴 PDF = 7) +- 제목 +26.4px 만큼 1쪽 콘텐츠가 아래로 밀리며 1쪽 끝의 `다단나누기` band 하나가 2쪽으로 → 연쇄 → 8쪽. +- 한컴은 제목 정합 상태로도 7쪽에 담으므로, rhwp 2~8쪽이 한컴보다 콘텐츠가 짧지 않다는 뜻 — 즉 **band-transition spacing deficit(제목↔헤더 ~15px, 헤더↔본문 ~20px; stage5)이 미해결**이라 한쪽으로만 늘어남. 더불어 기존 3쪽 overflow(콘텐츠 y=766 > body_bottom 758.4; pre-change 에도 존재한 버그)가 page break 로 풀리며 +1쪽에 기여. + +### ⚠ 문제 2 — svg_snapshot 2건 시프트 (회귀/개선 미확정) +- `cargo test --test svg_snapshot`: `issue_267_ktx_toc_page` (목차 제목 y 129.0 → 132.8, +3.8px), `issue_617_exam_kor_page5` (셀 "6" y 179.1 → 186.7, +7.6px; "홀수형" y 169.9 → 174.8, +4.9px) 두 건 FAILED. 나머지 6건 통과. +- 두 문서 모두 섹션-시작 문단(`para_index==0`)에 `spacing_before > 0` 이 있어 변경 영향. 해당 문서 한컴 PDF 대조 없이는 개선(정합)인지 회귀(한컴은 페이지-top 섹션-시작에서 드롭)인지 판정 불가. + +### ⚠ 문제 3 — 사용자 주 증상 미해소 +- 사용자 보고 "모든 구분 칸 위·아래 줄 간격 좁음" 의 핵심은 `다단나누기` band(섹션 헤더 띠 = 1×1 TAC 표) 주변 간격인데, 이들 band-top 문단은 `spacing_before == 0` 이라 본 변경으로 바뀌지 않음. band-transition spacing 은 별도 근원(TAC 표 띠 line-height / `다단나누기` zone 전환 암묵 간격) — RFC #774 영역. + +## 판단 요청 (작업지시자) + +| 옵션 | 내용 | 평가 | +|------|------|------| +| A | 제목 정정 유지 + 2 snapshot 한컴 PDF 검증 → 개선이면 `UPDATE_GOLDEN`, 회귀면 조건 추가 narrow + 페이지수 7→8 수용 | 제목만 정합. 페이지수 한컴 불일치 잔존. 주 증상 미해소. | +| B (권고) | 본 변경 revert → #853 을 진단·RFC #774 선행 타스크로 전환. band-transition spacing(TAC 표 띠 + `다단나누기` zone 전환) 본질 분석 후 제목·band·페이지수·overflow 일괄 정정 | `feedback_essential_fix_regression_risk` 정합. 부분 정정의 페이지수 회귀·snapshot 불확정·주 증상 미해소 회피. | +| C | 계속 진행 — band-transition spacing 정정 (RFC #774 흡수) | 광역 회귀 위험 큼. 한컴 2010/2020 정답지 + 전 fixture sweep 필수. 장기 작업. | + +권고: **B**. 본 변경은 1쪽 제목 1건만 정합되고 페이지 수가 한컴(7쪽)과 어긋나며(8쪽) snapshot 2건이 불확정 상태가 되고 정작 사용자 주 증상(band 간격)은 그대로다. RFC #774(한컴 paragraph/zone spacing 알고리즘) 선행 분석 후 일괄 정정이 안전. + +## 첨부 +- 변경 후 SVG: `output/svg/sc853b/` (8개) +- `cargo test --test svg_snapshot`: 6 passed / 2 failed (issue_267, issue_617). 나머지 `cargo test --release`: 본 변경 전 전건 통과 확인됨(svg_snapshot 외). diff --git a/mydocs/working/task_m100_853_stage2_v2.md b/mydocs/working/task_m100_853_stage2_v2.md new file mode 100644 index 000000000..63c23d4a6 --- /dev/null +++ b/mydocs/working/task_m100_853_stage2_v2.md @@ -0,0 +1,37 @@ +# Stage 2 (재개) 완료 보고서 — Task #853 (M100) — 섹션-top 제목 spacing_before 클램프 + +GitHub Issue: edwardkim/rhwp#853 · 브랜치: `local/task853` +배경: Stage 2(초안)에서 부분 정정 시도 후 작업지시자 옵션 B 로 일시 revert. 이후 작업지시자가 범위를 "제목 + band 간격 + overflow 전부"(옵션 C)로 확대 → 본 정정 재적용. + +## 변경 + +`src/renderer/layout/paragraph_layout.rs::layout_composed_paragraph` (745-748 부근): +```rust +let is_column_top = (y - col_area.y).abs() < 1.0; +if start_line == 0 && spacing_before > 0.0 { + if !is_column_top { + y += spacing_before; + } else if para_index == 0 { + let vpos0_px = para + .and_then(|p| p.line_segs.first()) + .map(|ls| hwpunit_to_px(ls.vertical_pos, self.dpi)) + .unwrap_or(0.0); + y += spacing_before.min(vpos0_px.max(0.0)); + } +} +``` +- column-top(`is_column_top`)이면서 **섹션 첫 문단(`para_index == 0`)** 인 경우, `spacing_before` 를 그 문단 첫 LINE_SEG 의 `vertical_pos`(한컴이 파일에 기록한 실제 렌더 첫 줄 위치)로 상한 클램프해 적용. 페이지 break 후 이어진 column-top(`para_index > 0`, `vertical_pos` 가 원래 레이아웃 위치를 담아 0 이 아닐 수 있음)은 종전대로 0. + +## 결과 + +- ✅ shortcut.hwp 제목 "글 2010 단축키 일람표" baseline y=79.4 → **105.8** (+26.4px). top ≈ 83.8px ≈ 한컴 PDF top 83.6px. 한컴 기록값 `vertical_pos=1984 HU (26.45px)` 와 정합. `height_measurer`(이미 26.5px 포함) ↔ `paragraph_layout` 비대칭 해소. +- ✅ `cargo test --release` 전건 통과(34 test suites ok, 0 failed). +- 🔄 svg_snapshot 2건 golden 갱신(`UPDATE_GOLDEN=1`): `tests/golden_svg/issue-267/ktx-toc-page.svg`(목차 제목 y 129.0 → 132.8), `tests/golden_svg/issue-617/exam-kor-page5.svg`(셀 "6" y 179.1 → 186.7, "홀수형" y 169.9 → 174.8). 두 문서의 섹션-시작 문단도 LINE_SEG.vertical_pos 기준으로 재배치 — 한컴이 파일에 기록한 위치와 정합하므로 개선으로 판단. (`is_column_top` 예외가 한컴이 *드롭*한 경우엔 `vertical_pos==0` 이라 `min(sb,0)=0` 으로 무변화 — 자기 정합적.) +- ⚠ shortcut.hwp 페이지 수 7 → 8. 제목 +26.4px 가 1쪽 band 하나를 2쪽으로 밀어 연쇄. 한컴은 7쪽이므로 rhwp 2~8쪽이 한컴보다 짧지 않다는 뜻 — band-transition deficit(Stage 3)이 미해결이라 한쪽으로만 늘어남. + 기존 3쪽 overflow(pre-change 버그)가 page break 로 풀리며 +1쪽 기여. Stage 3(band 정정) 후 7쪽 회복 가능 여부 재확인. + +## 다음 (Stage 3) + +`다단나누기` 구분 칸 band 위·아래 간격(~17px zone gap + ~20px 헤더 띠 텍스트 line0 흡수) + 3쪽 overflow(#768 패턴). composer/table layout 변경 영역이라 회귀 위험 큼 → Stage 3-1(추가 진단·설계) → 승인 → Stage 3-2(구현) 로 분리. 상세는 구현 계획서 v3 (`mydocs/plans/task_m100_853_impl_v3.md`). + +## 첨부 +- 변경 후 SVG: `output/svg/sc853/` (8개) diff --git a/mydocs/working/task_m100_853_stage3.md b/mydocs/working/task_m100_853_stage3.md new file mode 100644 index 000000000..b2ffc8e5e --- /dev/null +++ b/mydocs/working/task_m100_853_stage3.md @@ -0,0 +1,48 @@ +# Stage 3-1 조사 보고 — Task #853 (M100) — `다단나누기` 구분 칸 band 간격 + 3쪽 overflow + +GitHub Issue: edwardkim/rhwp#853 · 브랜치: `local/task853` · 상태: **조사 — 미수정, 설계·승인 대기** + +## 측정 (shortcut.hwp 2쪽, `pdf/basic/shortcut-2022.pdf` ↔ rhwp SVG `output/svg/sc853/shortcut_002.svg`) + +`mutool draw -r 100` PNG 픽셀 측정(@96dpi 환산) ↔ SVG 좌표: + +| 요소 | 한컴 PDF (body_top=56.7px 기준) | rhwp | 차이 | +|------|------------------|------|------| +| "파일" 헤더 띠(표) 상단 | +19.1px (5.1mm) | +3.8px | rhwp ~15px 높음 | +| 본문 첫 줄 "새 문서" 상단 | +134.3px (35.5mm) | ~+27px | rhwp **~107px 높음** | +| 띠 ↔ 본문 사이 간격 | ~92px | ~2px | rhwp **~90px 부족** | + +→ 사용자 보고("모든 구분 칸 위·아래 줄 간격 좁음")의 실체: 헤더 띠 자체가 ~15px 위로 + 띠↔본문 사이가 ~90px 부족. 1쪽 stage5 측정(헤더↔본문 ~20px)보다 2쪽은 훨씬 큼 — 2쪽은 `쪽나누기`로 시작하는 페이지. + +## IR 구조 (pi=36 "파일" 헤더 문단) + +``` +--- 문단 0.36 --- cc=19, text_len=2("파일"), controls=2 [쪽나누기] + [PS] ps_id=2 align=Justify spacing: before=0 after=0 line=100/Percent margins: left=0 right=2000 bf=3 + ls[0]: ts=0, vpos=0, lh=1200, th=1200, bl=1020 ← 텍스트 줄 (16px) + ls[1]: ts=10, vpos=1200, lh=2332, th=2332, bl=1982 ← 표 줄 (31px = 표 1766 + outer_margin 283×2) + [0] 단정의: 1단, 유형=일반, 간격=0.0mm + [1] 표: 1행×1열, 쪽나눔=RowBreak, padding=(283,283,283,283), size=69448×1766(245×6.2mm), bf=4 + outer_margin (283,283,283,283)=1mm, 셀[0] paras=1 text="파일" +``` +- 한컴은 pi=36 을 **line0=텍스트("파일", 16px) → line1=표(31px)** 순으로 배치(총 47px). rhwp 는 표를 line0 에 놓고 텍스트 line0 을 흡수 → ~27px (15~20px 부족). +- 표 셀 안에도 "파일" 이 들어 있어 PDF 상 띠에 "파일" 1개만 보이지만, 문단 텍스트 "파일"(line0)은 띠 위쪽 16px 줄에 별도로 흐른다(시각적으로는 띠와 겹치거나 거의 붙음). + +## 미규명 — 띠↔본문 ~92px 간격의 출처 + +pi=36 의 LINE_SEG 2 줄(16+31=47px)로는 ~92px 가 설명되지 않는다. pi=36 과 pi=37("새 문서") 사이에 ① 빈 문단/추가 LINE_SEG, ② 1단 zone → 2단 zone 전환 시 한컴이 두는 고정 간격, ③ `쪽나누기` + `다단나누기` 조합의 누적 offset 해석, ④ TAC 표 `wrap=위아래`(TopAndBottom)가 글자처럼 취급이면서도 위아래 어울림으로 예약하는 추가 높이 중 하나로 추정 — 정확한 규칙 미확정. RFC #774("한컴 paragraph/zone spacing 알고리즘 정밀 분석") 영역. + +## 3쪽 overflow + +3쪽 단3 `<편집 화면 분할에서>`(pi=94)·"화면 이동"(pi=95) 둘 다 `vpos=0` 겹침 — 닫힌 #768 패턴. 위 zone-transition 규칙 규명에 종속될 가능성 큼. + +## 권고 + +Stage 3 구현 전 **RFC #774 분석 문서 선행** 권고 — pi=36 의 line0/line1 배치 + zone 전환 간격(~92px) + TAC `wrap=위아래` 예약 + `쪽나누기` 누적 offset 을 PDF·IR 대조로 명세화한 뒤 composer/`layout_table_item`/`build_columns` 를 정정해야 안전(`feedback_essential_fix_regression_risk` — composer 변경은 전 문서 회귀 위험). 추측 구현 시 광역 회귀 위험 큼. + +판단 요청: +- A. RFC #774 분석 문서(`mydocs/tech/`)를 본 타스크에서 작성 → 승인 → 구현 +- B. composer 의 헤더 띠 line0 텍스트 렌더만 우선 시도(~15px 회복, 부분) + 나머지(~90px gap)는 후속 — `cargo test` 가드 +- C. Stage 3 보류, 본 타스크는 Stage 2(제목 정정)로 마무리 + #853 에 band/overflow 잔존 기록 + +(현재까지 커밋: Stage 2 제목 정정 + golden 2건 갱신 — `f0d34713`. 소스 추가 변경 없음.) diff --git a/mydocs/working/task_m100_853_stage3_v2.md b/mydocs/working/task_m100_853_stage3_v2.md new file mode 100644 index 000000000..fc7fcfbcf --- /dev/null +++ b/mydocs/working/task_m100_853_stage3_v2.md @@ -0,0 +1,29 @@ +# Stage 3-2 (1차 시도) 보고 — Task #853 (M100) — 헤더 띠 line0 텍스트 렌더 + +GitHub Issue: edwardkim/rhwp#853 · 브랜치: `local/task853` · 상태: **1차 시도 효과 없음 → revert. 추가 진단 필요.** + +## 시도한 변경 (revert 함) + +`src/renderer/pagination/engine.rs::place_table_fits`: +- `pre_table_end_line` 계산에 분기 추가: `is_tac_table && total_lines > 1` 이면 표 컨트롤이 놓인 LINE_SEG 인덱스(`control_text_positions()[ctrl_idx]` ↔ `line_segs[i].text_start` 비교, `find_inline_control_target_page` 와 동일 방식)를 `pre_table_end_line` 로 사용 → 표 앞 줄(텍스트)을 표보다 먼저 배치. +- `post_table_start` TAC 분기: `pre_table_end_line.max(1)` → `(pre_table_end_line + 1).min(total_lines).max(1)`. + +## 결과 + +빌드 성공, **shortcut.hwp SVG byte-identical (변화 없음)** — pi=36("파일" 헤더 띠)이 여전히 표를 line0 에 놓고 텍스트를 흡수, 8쪽 유지. `cargo test` 미실행(효과 없어 의미 없음). + +→ pi=36 의 TAC 표가 `place_table_fits` 의 `pre_table_end_line` 경로를 타지 않거나, `pre_table_end_line` 계산이 0 으로 떨어짐(추정: `control_text_positions()` 의 단위 ↔ `line_segs.text_start`(UTF-16) 불일치, 또는 pi=36 이 `쪽나누기`로 새 페이지 시작 시 다른 경로로 처리). revert. + +## 다음 — 추가 진단 필요 (Stage 3-1b) + +pi=36 의 레이아웃 경로를 디버그 계측으로 추적해야 함: +- `RHWP_LAYOUT_DEBUG=1` 등으로 pi=36 의 ComposedLine 수, MeasuredParagraph.line_heights, 어느 함수(`place_table_fits` / `split_table_rows` / 다른 경로)가 PageItem 을 생성하는지, `control_text_positions()`[table] 값과 `line_segs.text_start` 값을 출력. +- 그 결과로 "표가 놓인 줄 인덱스" 를 정확히 산출하는 위치/방법 확정 후 재시도. + +회귀 위험: composer/pagination 변경은 전 문서 영향 → 매 변경 후 `cargo test --release` + 전 fixture sweep 필수(`feedback_essential_fix_regression_risk`). + +## 현재 상태 정리 + +- Stage 2(섹션-top 제목 정정) = 커밋 `f0d34713`, 유지. shortcut.hwp 제목 정합, `cargo test` 전건 통과, golden 2건 갱신. +- Stage 3(band 간격 + overflow) = 미완. 1차 시도 효과 없음, 추가 진단 대기. +- 소스 추가 변경 없음(engine.rs revert 됨). 빌드 바이너리는 무효과 변경 포함 상태 → 다음 작업 시 재빌드 필요. diff --git a/mydocs/working/task_m100_853_stage3_v3.md b/mydocs/working/task_m100_853_stage3_v3.md new file mode 100644 index 000000000..1b80d0290 --- /dev/null +++ b/mydocs/working/task_m100_853_stage3_v3.md @@ -0,0 +1,40 @@ +# Stage 3-2 (2차 시도, 성공) 보고서 — Task #853 (M100) — 헤더 띠 line0 텍스트 렌더 + +GitHub Issue: edwardkim/rhwp#853 · 브랜치: `local/task853` +선행: Stage 3-1 분석(`task_m100_853_stage3.md`, `mydocs/tech/hancom_zone_paragraph_spacing.md`), Stage 3-2 1차 시도 실패(`task_m100_853_stage3_v2.md`). + +## 1차 실패 원인 (규명) + +1차는 `pagination/engine.rs::place_table_fits` 를 고쳤으나 효과 없음 — shortcut.hwp 는 **`typeset.rs` 경로**를 타며 `engine.rs::paginate_table_control` 은 호출되지 않음(디버그 print 미발화로 확인). 실제 헤더 띠 PageItem 생성 지점은 `typeset.rs::place_table_with_text`. 또 `control_text_positions()` 는 `char_offsets` 가 비면 무용(`[0, 0]` 반환)이라 "표가 놓인 줄 인덱스" 산출에 못 씀. + +## 2차 변경 (성공) — `src/renderer/typeset.rs::place_table_with_text` + +- `pre_table_end_line` 계산에 분기 추가: `table.common.treat_as_char && total_lines > 1 && para.text.chars().any(is_alphanumeric)` 이면, **표 줄의 높이(표 본체 + outer_margin top/bottom)와 일치하는 LINE_SEG 인덱스**를 `pre_table_end_line` 로 사용 → 그 앞 줄(텍스트)을 `PageItem::PartialParagraph{0..pre}` 로 표보다 먼저 emit. + - `control_text_positions()` 대신 `line_height` 매칭으로 표 줄을 찾음(char_offsets 무관). + - PUA 필러/공백만 있는 문단(복학원서.hwp pi=16 등 — 한컴이 표 폭만큼 필러로 줄바꿈시킨 케이스)은 `is_alphanumeric()` 가 false 라 제외 → `compute_tac_leading` 경로 유지(Task #842 결함 #2 정합). +- `tac_wrap_split` 플래그(`treat_as_char && pre>0 && pre body_bottom 시 다음 페이지) — overflow 16건 / 페이지 수 정합. + +## 커밋 +Stage 2 (`f0d34713`) → Stage 3-1 분석(`bd9d5148`/`0fca34ed`) → Stage 3-2 1차 실패 보고(`c765e36b`) → 본 변경(`typeset.rs` +33/-2) + 보고서. diff --git a/mydocs/working/task_m100_853_stage3_v4.md b/mydocs/working/task_m100_853_stage3_v4.md new file mode 100644 index 000000000..9829ec007 --- /dev/null +++ b/mydocs/working/task_m100_853_stage3_v4.md @@ -0,0 +1,31 @@ +# Stage 3-3 조사 보고 — Task #853 (M100) — 잔존 zone-transition gap + +GitHub Issue: edwardkim/rhwp#853 · 브랜치: `local/task853` · 상태: **조사 — 미수정. 한컴 정답지 cross-check 필요(RFC #774).** + +## 측정 (Stage 3-2 적용 후 상태) + +`process_multicolumn_break` 가 새 zone 의 `zone_y_offset` 를 `직전 문단 last_seg.vertical_pos + line_height + line_spacing` 로만 계산 → 한컴 PDF 대비 부족: + +| 페이지 | 요소 | 한컴 PDF (body_top 기준) | rhwp | 차이 | rhwp 산식 | +|--------|------|------------------|------|------|-----------| +| 1 | "커서 이동" 헤더 띠 | +87.6px | +72.9px | ~15px 부족 | 제목 zone vpos_end(69.1) | +| 1 | 본문 첫 줄 | +137.9px | +100.2px | **~38px 부족** | 69.1 + 헤더 zone vpos_end(31.1) | +| 2 | "파일" 헤더 띠 | +19.1px | +19.8px | ✓ (Stage 3-2) | line0(16) + outer_margin | +| 2 | 본문 첫 줄 | ~+75px | ~+45px | **~28~30px 부족** | 헤더 zone vpos_end(47.1) | + +## 분석 + +### 페이지 1 — ~38px ≈ ColumnDef `간격` 10mm (37.8px) +pi=1(헤더 띠)의 `다단나누기` ColumnDef = `1단, 간격=10.0mm(2835HU=37.8px)`. pi=2(본문)의 ColumnDef = `2단, 배분, 간격=1.0mm`. pi=0(제목)의 ColumnDef = `1단, 간격=0mm`. 한컴은 이 1단 ColumnDef 의 `간격`(원래 단 사이 가로 간격이지만 1단이라 가로로는 무의미)을 **세로 zone 진입 간격**으로 쓰는 것으로 보임 — 본문 부족분(~38px) = pi=1 의 `간격` 10mm 와 일치. 다만 제목↔헤더 +14.7px / 헤더↔본문 +23px 로 정확히 50/50 분할은 아님(≈40/60) → 정확한 분배 규칙 미확정. + +### 페이지 2 — ~28px 미규명 +pi=36(헤더 띠)의 ColumnDef = `1단, 간격=0mm`. 따라서 §1 의 ColumnDef-간격 가설로는 0px → 페이지 2 본문 부족분 ~28px 가 설명 안 됨. 후보(분석 문서 §5): TAC 표 `wrap=위아래(TopAndBottom)` 가 글자처럼 취급이면서도 위아래 어울림으로 추가 예약 / `쪽나눔=RowBreak(attr=0x04000006)` 처리 / 1단→2단 zone 전환 고정 간격. 3쪽 이후 띠들 PDF↔IR 추가 측정 + 한컴 편집기(Windows) cross-check 필요. + +## 권고 + +§1(ColumnDef 간격 → zone 진입 간격)은 페이지 1 을 개선하지만 분배 규칙이 불확정이고, §2(페이지 2 ~28px)는 출처 미규명이라, 추측 구현 시 광역 회귀 위험 큼(`feedback_essential_fix_regression_risk`, `feedback_rule_not_heuristic`). **이 두 항목은 한컴 정답지 cross-check + 추가 샘플 측정 후 처리** 권고. + +본 타스크는 **Stage 2(섹션-top 제목 정정) + Stage 3-2(헤더 띠 line0 텍스트 배치)** 로 마무리 — 둘 다 한컴 PDF 정합 + `cargo test` 전건 통과 + svg_snapshot 8/8. 잔존(zone-transition gap §1/§2, 페이지 수 7≠8, overflow 16건)은 분석 문서 `mydocs/tech/hancom_zone_paragraph_spacing.md` 에 정리 — 후속 타스크에서 RFC #774 와 함께 처리. + +## 커밋 +Stage 2(`f0d34713`) → Stage 3-1 분석(`bd9d5148`/`0fca34ed`) → Stage 3-2(`c765e36b` 실패보고 + `1f7328b2` 성공) → 본 조사 보고. 소스 추가 변경 없음. diff --git a/mydocs/working/task_m100_866_stage1.md b/mydocs/working/task_m100_866_stage1.md new file mode 100644 index 000000000..9fc5d28c9 --- /dev/null +++ b/mydocs/working/task_m100_866_stage1.md @@ -0,0 +1,39 @@ +# Stage 1 완료 보고서 — Task #866 (M100) — PDF 측정 + IR 대조 + +GitHub Issue: edwardkim/rhwp#866 · 브랜치: `local/task866` (← `pr-task853`) · 코드 미수정 (측정 전용) + +## 1. PDF 측정 (`pdf/basic/shortcut-2022.pdf`, `mutool draw -r 100`, @96dpi 환산, body_top=15mm 기준) + +| 페이지 | 헤더 | 헤더 띠 상단 | 헤더 띠 하단 | 본문 첫 줄 상단 | 띠 하단↔본문 | +|--------|------|-------------|-------------|----------------|-------------| +| 2 | "파일" (pi=36) | +19.1px | +43.1px | +74.8px | **+31.7px** | +| 3 | "보기" (pi=81) | +25.8px | +49.8px | +83.4px | **+33.6px** | +| 4 | (헤더 없음 — 내용 흐름) | — | — | +69.0px(이전 zone 연속) | — | +| 5 | (헤더 없음) | — | — | — | — | + +## 2. rhwp 상태 (PR #868 Task #853 적용 후) + +- 2쪽: 헤더 띠 +19.8px ✓ / 하단 +43.3px ✓ / 본문 zone_y_offset = +47.1px(= pi=36 `vpos_zone_height` = ls[1].vpos 1200 + lh 2332 + ls 0 = 3532HU) → 띠 하단↔본문 ≈ **+4px**. **PDF(+31.7px) 대비 ~28px 부족.** +- 3쪽: pi=81 은 IR 상 LINE_SEG 1개(`ls[0]: vpos=0, lh=1200, ls=480`) — `vpos_zone_height` = 1680HU = 22.4px (표 23.5px 보다도 작음). dump-pages 의 본문 zone_y_offset = 59.9px (layout 의 표 band 처리로 보정됨). PDF 본문(+83.4px) 대비 ~24px 부족. + +## 3. 분석 — 후보 검토 + +### 띠 하단↔본문 gap ≈ ~32px ≈ TAC 표 band 의 line height (~31px) +- pi=36 의 line1(표 줄) lh = 2332 HU = 31.1px (= 표 본체 1766 + outer_margin 283×2 = 566). PDF gap ≈ 31.7px ≈ 이 값. pi=81 도 ≈ 33.6px ≈ 비슷. +- 가설: 한컴은 `wrap=위아래(TopAndBottom)` 인 TAC 표를 (a) 인라인으로 line(text line 다음 줄)에 배치 + (b) `위아래` 어울림으로 표 band 높이만큼 그 아래에 *추가* 예약 → 본문이 `(line0)+(표 band 인라인)+(표 band 추가 예약)` 만큼 아래에서 시작. pi=36: 16 + 31 + 31 ≈ 78px ≈ PDF 74.8px(±3px). pi=81 도 ≈ 84px ≈ PDF 83.4px. + +### 미확정 — 게이팅 조건이 불명확 + 회귀 위험 +- 1쪽 헤더 띠(pi=1, `text_len=0`, ColumnDef `간격=10mm`)는 PR #868 Stage 3-3(ColumnDef 간격 → zone 진입 간격)로 **이미 PDF 정합**(헤더 zone +88px, 본문 +138px ≈ PDF +87.6/+137.9px). 여기에 "표 band 추가 예약(~31px)"을 또 더하면 overshoot. → 위 가설은 1쪽 pi=1 에는 적용되면 안 됨. +- 차이점: pi=1 은 `text_len=0`(line0 텍스트 없음, ColumnDef 간격 10mm), pi=36 은 `text_len=2`(line0 텍스트, ColumnDef 간격 0mm), pi=81 은 `text_len=2`·LINE_SEG 1개·ColumnDef 간격 0mm·PS `line=140%`(pi=36 은 100%) — IR 구조가 페이지마다 제각각이라 "표 band 추가 예약" 적용 조건을 룰로 확정하기 어려움(`feedback_rule_not_heuristic` 위배 소지). +- 또 본문 zone_y_offset 계산은 `process_multicolumn_break`(`vpos_zone_height`) + `layout_table_item`(표 band 렌더) 두 곳이 협조해야 하므로 양쪽 동시 수정 필요 → composer/typeset/layout 광역 회귀 위험(`feedback_essential_fix_regression_risk`). 닫힌 PR #771/Issue #770(line0 흡수 ~16px 만 다룸), 닫힌 #773/#776, RFC #774 가 모두 이 ~28px 를 못 닫은 이유. + +## 4. 권고 — 보류 + +PDF 측정만으로는 ~32px 의 *기여 요소*(≈ 표 band 높이)는 좁혀졌으나, **적용 게이팅 조건이 IR 구조 차이로 확정 불가**하다(1쪽 pi=1 vs 2쪽 pi=36 vs 3쪽 pi=81 이 모두 다름). 한컴 편집기(Windows)에서 실제 layout 구조(표 band 의 위아래 예약 여부, 본문 paragraph 의 zone 진입 offset)를 직접 확인하지 않으면 추측 구현이 되고, composer/typeset/layout 협조 변경이라 회귀 위험이 큼. + +→ **본 타스크는 Stage 1(측정) 로 마감하고 #866 은 한컴 편집기 접근 가능 환경/시점까지 보류** 권고. 측정 데이터·가설은 본 보고서 + `mydocs/tech/hancom_zone_paragraph_spacing.md` §5 에 보존. (#770 코멘트 "필요 시 신규 이슈로 재등록" 의 #866 이 그 신규 이슈이며, 본 Stage 1 으로 가설을 한 단계 좁힘.) + +만약 강행한다면: Stage 2 = `process_multicolumn_break` 의 `vpos_zone_height` 에 "직전 zone 의 마지막 paragraph 가 `wrap=위아래` TAC 표 보유 & multi-line(line0 텍스트 존재) & ColumnDef 간격=0 이면 표 band 높이(`table.common.height + outer_margin_top + outer_margin_bottom` px)를 추가" + `layout_table_item` 에 동일 advance 보장 → `cargo test` + 전 fixture sweep + `pdf-2020/` 대조. 단 게이팅 조건이 휴리스틱이므로 비권장. + +## 산출물 +- 본 보고서 + 측정 PNG(`/tmp/sc_p{2..5}.png` — 임시). 소스 변경 없음. diff --git a/mydocs/working/task_m100_866_stage2.md b/mydocs/working/task_m100_866_stage2.md new file mode 100644 index 000000000..488508b14 --- /dev/null +++ b/mydocs/working/task_m100_866_stage2.md @@ -0,0 +1,34 @@ +# Stage 2 진행 보고 — Task #866 (M100) — 추가 조사, 미수정 + +GitHub Issue: edwardkim/rhwp#866 · 브랜치: `local/task866` (← `pr-task853`) · 상태: **조사 — 미수정, 보류 권고.** + +## 추가 측정·분석 결과 + +Stage 1 측정(2쪽 띠↔본문 +31.7px, 3쪽 +33.6px)에 더해 IR 구조 정밀 대조: + +| | 1쪽 (pi=1) | 2쪽 (pi=36) | 3쪽 (pi=81) | +|---|---|---|---| +| text_len | 0 (빈 문단) | 2 ("파일") | 2 ("보기") | +| LINE_SEG | ls[0] vpos=0 lh=2332 (표 줄 1개) | ls[0] vpos=0 lh=1200 (텍스트) + ls[1] vpos=1200 lh=2332 (표) | ls[0] vpos=0 lh=1200 ls=480 (1개; 표가 이 줄에) | +| PS line | 100% | 100% | 140% | +| ColumnDef 간격 | **10.0mm** | 0.0mm | 0.0mm | +| rhwp(PR #868) 본문 zone offset | +138.0px (= 69.1 제목 + 5mm + 31.1 헤더 + 5mm, Stage 3-3 의 10mm ColumnDef 적용) | +47.1px | +59.9px | +| 한컴 PDF 본문 위치 | +137.9px ✓ | +74.8px | +83.4px | +| 차이 | 0 (정합) | **~28px 부족** | **~24px 부족** | + +### 가설 (측정 기반) +2·3쪽의 부족분(~28~31px) ≈ TAC 표 band 의 line height(2332 HU ≈ 31.1px). 즉 한컴은 이 헤더 띠 표(`treat_as_char` + `wrap=위아래`) 아래에 표 band 높이만큼을 *추가로* 비워두는 것으로 보임 — pi=36: line0(16) + 표band 인라인(31) + 표band 추가(31) ≈ 78px ≈ PDF 74.8px. pi=81: line0(22.4) + 31 + 31 ≈ 84.6px ≈ PDF 83.4px. + +### 미해결 — 게이팅 조건 불명확 +- 1쪽 헤더 띠(pi=1)는 `text_len=0` + ColumnDef `간격=10mm` 이라 PR #868 Stage 3-3(ColumnDef 간격/2 분배)로 **이미 정합**. 여기에 "표band 추가 예약(~31px)"을 더하면 overshoot → 위 가설은 pi=1 에는 적용되면 안 됨. +- 즉 1쪽은 "ColumnDef 간격 10mm" 이 extra 의 출처이고, 2·3쪽은 "표band ~31px 추가"가 extra 의 출처 — **서로 다른 메커니즘**으로 보이는데, 이 두 가지가 사실은 하나의 규칙인지(예: ColumnDef 간격이 0 일 때만 표band 추가) 아니면 진짜 별개인지 측정만으로 확정 불가. ColumnDef 간격=0 이면 표band 추가, >0 이면 ColumnDef 간격 — 이라는 규칙은 가능하나 검증 표본이 shortcut.hwp 한정이라 일반 룰로 확정 못 함(`feedback_rule_not_heuristic`). +- 또 본문 zone offset 은 `process_multicolumn_break`(`vpos_zone_height`) + `layout_table_item`(표 band advance) 두 곳 협조 변경 필요 → composer/typeset/layout 광역 회귀 위험(`feedback_essential_fix_regression_risk`). 닫힌 PR #771/#770/#773/#776/RFC #774 가 이 ~28px 를 못 닫은 이유. + +## 권고 — 보류 + +기여 요소(≈ 표 band 높이)와 강력한 가설은 확보했으나, 적용 게이팅(1쪽 vs 2·3쪽의 메커니즘 차이가 단일 룰인지)을 확정할 추가 표본/검증 수단(다른 문서의 동일 패턴, 한컴 편집기 직접 확인)이 없다. **#866 은 본 보고서까지로 마감하고, 검증 수단이 확보되는 시점에 가설(표 band 추가 예약, ColumnDef 간격=0 게이팅)을 검증·구현** 권고. 가설·측정은 본 보고서 + `mydocs/tech/hancom_zone_paragraph_spacing.md` §5 에 보존. + +만약 강행한다면 Stage 3 = `process_multicolumn_break` 에서 "직전 zone 마지막 paragraph 가 `wrap=위아래` TAC 표 보유 & ColumnDef 간격=0 이면 `vpos_zone_height` 에 표 band 높이(`table.common.height + outer_margin_top + outer_margin_bottom` px) 추가" + `layout_table_item` 의 advance 정합 → `cargo test --release` + 전 fixture sweep + shortcut.hwp 7~8쪽 SVG↔`pdf/`(2022)·`pdf-2020/`. 게이팅이 표본 1개 기반이라 비권장. + +## 산출물 +- 본 보고서. 소스 변경 없음. diff --git a/mydocs/working/task_m100_866_stage3.md b/mydocs/working/task_m100_866_stage3.md new file mode 100644 index 000000000..7e1250d19 --- /dev/null +++ b/mydocs/working/task_m100_866_stage3.md @@ -0,0 +1,34 @@ +# Stage 3 완료 보고서 — Task #866 (M100) — 헤더 띠↔본문 gap 정정 + zone-간격 layout 미러 + +GitHub Issue: edwardkim/rhwp#866 · 브랜치: `local/task866` (← `pr-task853`) + +## 변경 + +### `src/renderer/typeset.rs::process_multicolumn_break` +- `tac_band_extra`: 직전 zone 의 마지막 paragraph 가 `wrap=위아래`(TopAndBottom)인 글자처럼-취급 표(헤더 띠)를 보유하고 그 zone 의 1단 ColumnDef 간격이 0 이면, `vpos_zone_height` 에 표 band 높이(`table.common.height + outer_margin_top + outer_margin_bottom` px)를 추가 → 다음 zone 진입 offset 을 그만큼 내림. (한컴 PDF 측정: 헤더 띠 하단↔본문 ~28~33px = 표 band 높이 ≈ 31px. ColumnDef 간격>0 인 헤더 띠(1쪽 등)는 그 간격이 이미 zone 여백이 되므로 제외.) +- 다단 zone 시작 여유 가드 임계값: `available - 3*one_line` → `available - 4*one_line`(표 band 추가로 zone 이 커진 만큼). + +### `src/renderer/layout.rs::build_columns` +- **핵심**: 이전엔 zone-간 세로 여백(PR #868 Stage 3-3 의 ColumnDef 간격, 본 Stage 3 의 tac_band)이 pagination 의 `current_zone_y_offset`(메타데이터)에만 반영되고 layout 의 zone 스태킹(`prev_zone_y_end` 누적)에는 안 반영되어 SVG 렌더에 미적용이었다. → layout 에도 미러: + - zone 전환 시 `current_zone_start_y += (이전 zone 디자인 spacing /2) + (새 zone 디자인 spacing /2)`. 디자인 spacing = 1단 ColumnDef 의 `간격`(다단은 0). pagination 의 process_multicolumn_break 와 동일 시멘틱. + - 헤더 띠 zone(마지막 item 이 `wrap=위아래` 글자처럼-취급 표 & 1단 ColumnDef 간격=0) 처리 후 `prev_zone_y_end += 표 band 높이`. + +## 결과 (shortcut.hwp ↔ `pdf/basic/shortcut-2022.pdf`, SVG baseline ↔ PDF baseline 추정) + +| | 변경 전(PR #868) | 변경 후 | 한컴 PDF | 평가 | +|---|---|---|---|---| +| 1쪽 본문 첫 줄 baseline (into body) | ~+111px (메타데이터 +138 이지만 렌더 미반영) | **+149.3px** | ~+148px | ✓ 정합 | +| 2쪽 본문 첫 줄 baseline | ~+58px | **+89.5px** | ~+85px | ~4.5px 초과 (대폭 개선, 종전 ~28px 부족) | +| 3쪽 본문 첫 줄 baseline | ~+71px | **+102.3px** | ~+93px | ~9px 초과 (개선, 종전 ~24px 부족) | +| `LAYOUT_OVERFLOW` | 0 (PR #868 = Stage 3-3c) / 원래 25 | **4** ⚠ | — | 4쪽 2단 본문 zone — #867 영역 | +| `cargo test --release` | 34/34 ✓ | **34/34 ✓** (svg_snapshot 8/8 golden 무변경) | — | 회귀 0 | + +→ 사용자 주 증상("모든 구분 칸 위·아래 줄 간격 좁음", 특히 헤더 띠↔본문 ~28px 부족)이 **전 페이지에서 해소**(잔여 ~4~9px 초과). PR #868 의 "1쪽 정합" 표기는 실제론 메타데이터만이었고 본 Stage 3 의 layout 미러로 비로소 시각 정합됨. + +## 잔존 + +- `LAYOUT_OVERFLOW` 4건(4쪽 2단 본문 zone, pi=143~147) — 헤더 띠가 ~31px 커진 만큼 후속 zone 들이 밀려 하단 초과. `process_multicolumn_break` 가드는 새 zone 의 *시작* 여유만 보므로, 시작은 들어가나 zone 콘텐츠가 넘치는 케이스는 못 잡음. 가드를 콘텐츠 높이 추정 기반으로 보강 필요 → **#867** 영역(페이지 수 7≠8 포함). +- 2·3쪽 ~4~9px 초과 — tac_band(`table.height + om_top + om_bot`)가 한컴 실측 gap(~28~33px)보다 미세하게 큼. 정밀화하려면 한컴 편집기 cross-check 필요(macOS 환경 한계). + +## 커밋 +PR #868(Task #853) → `local/task866`: 수행/구현 계획서 → Stage 1 측정 → Stage 2 분석 → 본 Stage 3 구현. diff --git a/src/renderer/layout.rs b/src/renderer/layout.rs index dcb5847fe..e34a1f1f3 100644 --- a/src/renderer/layout.rs +++ b/src/renderer/layout.rs @@ -1214,6 +1214,21 @@ impl LayoutEngine { let mut prev_zone_y_end: f64 = 0.0; let mut current_zone_start_y: f64 = 0.0; let mut last_zone_y_offset: f64 = -1.0; + // [Task #853/#866] 직전 zone 의 "디자인 spacing"(1단 ColumnDef 의 `간격`, 다단은 0). + // 한컴은 zone 전환 시 (이전 zone 디자인 spacing /2)+(새 zone /2) 만큼 세로 여백을 + // 둔다(shortcut.hwp 1쪽 헤더 띠 ColumnDef 간격=10mm → 제목↔헤더 5mm, 헤더↔본문 5mm). + // pagination 측 process_multicolumn_break 의 동작과 동일 시멘틱. + let design_spacing_of = |para_idx: usize| -> f64 { + paragraphs.get(para_idx) + .and_then(|p| p.controls.iter().find_map(|c| match c { + Control::ColumnDef(cd) if cd.column_count.max(1) <= 1 => + Some(hwpunit_to_px(cd.spacing as i32, self.dpi)), + Control::ColumnDef(_) => Some(0.0), + _ => None, + })) + .unwrap_or(0.0) + }; + let mut prev_zone_design_px: f64 = 0.0; // 다단 레이아웃: body_area 전체에 걸치는 TopAndBottom 개체의 예약 높이 // (한 단에만 할당되더라도 모든 단에 적용) @@ -1236,11 +1251,22 @@ impl LayoutEngine { let is_new_zone = (col_content.zone_y_offset - last_zone_y_offset).abs() > 0.1; if is_new_zone { + // 새 zone 의 디자인 spacing = 이 zone 첫 paragraph 의 ColumnDef `간격`(1단 한정). + let new_zone_first_para = col_content.items.first().map(|it| match it { + PageItem::FullParagraph { para_index } + | PageItem::PartialParagraph { para_index, .. } + | PageItem::Table { para_index, .. } + | PageItem::PartialTable { para_index, .. } + | PageItem::Shape { para_index, .. } => *para_index, + }); + let new_zone_design = new_zone_first_para.map(|pi| design_spacing_of(pi)).unwrap_or(0.0); if col_content.zone_y_offset > 0.0 { - current_zone_start_y = prev_zone_y_end; + current_zone_start_y = prev_zone_y_end + + prev_zone_design_px / 2.0 + new_zone_design / 2.0; } else { current_zone_start_y = 0.0; } + prev_zone_design_px = new_zone_design; last_zone_y_offset = col_content.zone_y_offset; } @@ -1269,6 +1295,32 @@ impl LayoutEngine { if y_offset > prev_zone_y_end { prev_zone_y_end = y_offset; } + // [Task #866] 헤더 띠 zone(wrap=위아래 인 글자처럼-취급 표 보유 + 1단 ColumnDef + // 간격=0)의 아래에는 한컴이 표 band 높이만큼 추가 여백을 둔다(한컴 PDF 측정: + // shortcut.hwp 2·3쪽 헤더 띠 하단↔본문 ~28~33px). 다음 zone 시작 y 를 그만큼 내림. + // ColumnDef 간격>0 인 헤더 띠(1쪽 등)는 그 간격이 이미 zone 여백이 되므로 제외. + // (pagination 측 process_multicolumn_break 의 tac_band_extra 와 동일 시멘틱.) + if let Some(last_para_idx) = col_content.items.last().and_then(|it| match it { + PageItem::Table { para_index, .. } => Some(*para_index), + _ => None, + }) { + if let Some(p) = paragraphs.get(last_para_idx) { + let cd_gap_zero = p.controls.iter().any(|c| matches!(c, + Control::ColumnDef(cd) if cd.column_count.max(1) <= 1 && cd.spacing == 0)); + if cd_gap_zero { + if let Some(band) = p.controls.iter().find_map(|c| match c { + Control::Table(t) if t.common.treat_as_char + && matches!(t.common.text_wrap, crate::model::shape::TextWrap::TopAndBottom) => + Some(hwpunit_to_px(t.common.height as i32, self.dpi) + + hwpunit_to_px(t.outer_margin_top as i32, self.dpi) + + hwpunit_to_px(t.outer_margin_bottom as i32, self.dpi)), + _ => None, + }) { + prev_zone_y_end += band; + } + } + } + } body_node.children.push(col_node); } } diff --git a/src/renderer/layout/paragraph_layout.rs b/src/renderer/layout/paragraph_layout.rs index 1a7b8520b..d14e73253 100644 --- a/src/renderer/layout/paragraph_layout.rs +++ b/src/renderer/layout/paragraph_layout.rs @@ -742,10 +742,23 @@ impl LayoutEngine { }; // 문단 앞 간격 (첫 줄일 때만) - // 단/페이지의 맨 처음 문단은 spacing_before 적용하지 않음 + // 단/페이지의 맨 처음 문단(column-top)은 spacing_before 를 통째 적용하면 한컴보다 + // 아래로 밀리므로 종전엔 0 으로 버렸다. 다만 섹션의 첫 문단(para_index==0, 예: 제목)은 + // 한컴 PDF 가 LINE_SEG.vertical_pos(실제 렌더한 첫 줄 흐름 위치)만큼 앞 간격을 두므로 + // (제목: spacing_before=52.9px 이지만 vertical_pos=26.5px), 그 경우 spacing_before 를 + // LINE_SEG.vertical_pos 로 상한 클램프해 적용한다. 페이지 break 후 이어진 column-top + // (para_index>0)은 종전대로 0. (Task #853) let is_column_top = (y - col_area.y).abs() < 1.0; - if start_line == 0 && spacing_before > 0.0 && !is_column_top { - y += spacing_before; + if start_line == 0 && spacing_before > 0.0 { + if !is_column_top { + y += spacing_before; + } else if para_index == 0 { + let vpos0_px = para + .and_then(|p| p.line_segs.first()) + .map(|ls| hwpunit_to_px(ls.vertical_pos, self.dpi)) + .unwrap_or(0.0); + y += spacing_before.min(vpos0_px.max(0.0)); + } } // 문단 전체에서 모든 라인의 runs가 비어있는지 확인 diff --git a/src/renderer/typeset.rs b/src/renderer/typeset.rs index f822f34fb..9dc8318a0 100644 --- a/src/renderer/typeset.rs +++ b/src/renderer/typeset.rs @@ -151,6 +151,21 @@ struct TypesetState { /// process_multicolumn_break 에서 새 ColumnDef 매칭 시 갱신. /// Distribute 다단의 짧은 컬럼 vpos-reset 검출 임계값 완화에 사용. current_zone_column_type: ColumnType, + /// [Task #853] 현재 zone 의 "디자인 spacing"(px) — 1단 ColumnDef 의 `간격` 값. + /// 한컴은 1단 ColumnDef 의 `간격`(가로 단 간격이지만 1단이라 무의미)을 zone 진입 + /// 세로 간격으로 쓴다(shortcut.hwp 1쪽 헤더 띠 = 10mm). zone 전환 시 + /// (이전 zone 디자인 spacing /2) + (새 zone 디자인 spacing /2) 를 zone_y_offset 에 + /// 더한다. 다단(2+) ColumnDef 의 `간격`은 가로 간격이므로 0 으로 둔다. + current_zone_design_spacing_px: f64, +} + +/// [Task #853] ColumnDef 의 "디자인 spacing"(px): 1단이면 `간격`, 다단이면 0. +fn column_def_design_spacing_px(cd: &ColumnDef, dpi: f64) -> f64 { + if cd.column_count.max(1) <= 1 { + hwpunit_to_px(cd.spacing as i32, dpi) + } else { + 0.0 + } } impl TypesetState { @@ -190,6 +205,7 @@ impl TypesetState { current_column_wrap_around_paras: Vec::new(), current_column_wrap_anchors: std::collections::HashMap::new(), current_zone_column_type: column_type, + current_zone_design_spacing_px: 0.0, } } @@ -388,6 +404,7 @@ impl TypesetEngine { column_def.column_type, ); st.hide_empty_line = hide_empty_line; + st.current_zone_design_spacing_px = column_def_design_spacing_px(column_def, self.dpi); // 머리말/꼬리말/쪽 번호/새 번호/감추기 컨트롤 수집 let (hf_entries, page_number_pos, new_page_numbers, page_hides) = @@ -439,6 +456,11 @@ impl TypesetEngine { st.current_zone_layout = Some(new_layout.clone()); st.layout = new_layout; st.current_zone_column_type = cd.column_type; + // [Task #853] 새 페이지 첫 zone: 디자인 spacing /2 (위쪽 절반)만 추가. + // (이전 zone 은 이전 페이지에 있었으므로 아래쪽 절반은 더하지 않음.) + let new_ds = column_def_design_spacing_px(cd, self.dpi); + st.current_zone_y_offset += new_ds / 2.0; + st.current_zone_design_spacing_px = new_ds; } } } @@ -467,9 +489,17 @@ impl TypesetEngine { // 단일 단/Normal 다단은 영향 없음. let is_distribute = st.col_count > 1 && matches!(st.current_zone_column_type, ColumnType::Distribute); + // [Task #853] Distribute 다단의 "1줄짜리 컬럼" 케이스: 직전 문단이 + // 단 1줄(예: vpos=0)이고 현재 문단도 vpos=0 이면 `cv < pv` 가 0<0 으로 + // 거짓이라 컬럼 전환을 못 잡았다(shortcut.hwp 스타일/속성 섹션). 직전 문단의 + // vpos+line_height(=콘텐츠 끝)를 기준으로 비교하면 정상 흐름(cv=pv_end+ls≥pv_end) + // 은 영향 없고 reset(cv≪pv_end)만 잡힌다. + let prev_vpos_end = prev_para.line_segs.last() + .map(|s| s.vertical_pos + s.line_height) + .unwrap_or(pv); let trigger = if st.col_count > 1 { if is_distribute { - cv < pv && pv > 0 + cv < prev_vpos_end && prev_vpos_end > 0 } else { cv < pv && pv > 5000 } @@ -1768,6 +1798,23 @@ impl TypesetEngine { let total_lines = fmt.line_heights.len(); let pre_table_end_line = if vertical_offset > 0 && !para.text.is_empty() { total_lines + } else if table.common.treat_as_char && total_lines > 1 + && para.text.chars().any(|c| c.is_alphanumeric()) + { + // 전폭 TAC 표가 자동 줄바꿈으로 자기 줄(line index N)에 놓인 경우(\n 없음): + // 한컴은 LINE_SEG 순서대로 line0=텍스트 → lineN=표 로 렌더한다. + // control_text_positions() 는 char_offsets 가 비면 무용하므로, 표 줄의 높이 + // (표 본체 + outer margin top/bottom)와 일치하는 LINE_SEG 인덱스로 판정한다. + // PUA 필러/공백만 있는 문단(예: 복학원서.hwp pi=16 — 한컴이 표 폭만큼 필러로 + // 줄바꿈시킨 케이스)은 is_alphanumeric() 가 false 라 제외 → compute_tac_leading + // 경로 유지. (Task #853, Task #842 결함 #2 의 PUA 필러 판정과 정합) + let om_top = hwpunit_to_px(table.outer_margin_top as i32, self.dpi); + let om_bot = hwpunit_to_px(table.outer_margin_bottom as i32, self.dpi); + let tbl_line_h = hwpunit_to_px(table.common.height as i32, self.dpi) + om_top + om_bot; + para.line_segs.iter().enumerate() + .find(|(_, ls)| (hwpunit_to_px(ls.line_height, self.dpi) - tbl_line_h).abs() < 1.0) + .map(|(i, _)| i) + .unwrap_or(0) } else { 0 }; @@ -1804,10 +1851,19 @@ impl TypesetEngine { // - Square wrap (어울림): max(pre_height, v_off + table_total) // 호스트 텍스트와 표가 같은 y 영역을 공유하므로 더 큰 쪽만 누적. // - 그 외 (TopAndBottom 등): pre_height + table_total 합산 (기존 동작). + // 전폭 TAC 표가 자기 줄(line index = pre_table_end_line)에 놓인 split 케이스: + // table_total_height(=fmt.height_for_fit)는 pre-text 줄까지 포함하므로 pre_height + // 를 따로 더하면 이중 계산이 된다. 또 표가 차지한 줄은 post-text 에서 제외해야 한다. + // (Task #853) + let tac_wrap_split = table.common.treat_as_char + && pre_table_end_line > 0 && pre_table_end_line < total_lines; + if is_wrap_around_table && pre_height > 0.0 { let v_off_px = crate::renderer::hwpunit_to_px(vertical_offset as i32, self.dpi); let table_bottom = v_off_px + table_total_height; st.current_height += pre_height.max(table_bottom); + } else if tac_wrap_split { + st.current_height += table_total_height; } else { st.current_height += pre_height + table_total_height; } @@ -1818,7 +1874,9 @@ impl TypesetEngine { let tac_table_count = para.controls.iter() .filter(|c| matches!(c, Control::Table(t) if t.attr & 0x01 != 0)) .count(); - let post_table_start = if table.attr & 0x01 != 0 { + let post_table_start = if tac_wrap_split { + (pre_table_end_line + 1).min(total_lines).max(1) + } else if table.attr & 0x01 != 0 { pre_table_end_line.max(1) } else if is_last_table && !is_first_table { 0 @@ -2339,7 +2397,50 @@ impl TypesetEngine { } else { st.current_height }; - st.current_zone_y_offset += vpos_zone_height; + // [Task #853] zone 전환 시 디자인 spacing(1단 ColumnDef 의 `간격`)을 세로 간격으로: + // (이전 zone 디자인 spacing /2) + (새 zone 디자인 spacing /2) 를 더한다. + // shortcut.hwp 1쪽: 제목 zone(0mm) → 헤더 띠 zone(10mm) → 본문 zone(2단, 0) + // → 제목↔헤더 = 5mm, 헤더↔본문 = 5mm (한컴 PDF 정합). + let new_ds = paragraphs[para_idx].controls.iter().find_map(|c| { + if let Control::ColumnDef(cd) = c { Some(column_def_design_spacing_px(cd, self.dpi)) } else { None } + }).unwrap_or(0.0); + // [Task #866] 직전 zone 의 마지막 paragraph 가 wrap=위아래 인 글자처럼-취급 표(헤더 띠)를 + // 보유하고 그 zone 의 1단 ColumnDef 간격이 0 이면, 한컴은 표 band 높이(표 본체 + + // outer_margin top/bottom)만큼을 표 아래에 추가로 비워둔다(한컴 PDF 측정: + // shortcut.hwp 2·3쪽 헤더 띠 하단↔본문 ~28~33px). ColumnDef 간격>0 인 헤더 띠(1쪽 + // 등)는 그 간격이 이미 zone 사이 여백이 되므로 제외. + let tac_band_extra: f64 = if st.current_zone_design_spacing_px < 0.5 { + (0..para_idx).rev() + .find(|&i| !paragraphs[i].line_segs.is_empty()) + .and_then(|pi| paragraphs[pi].controls.iter().find_map(|c| match c { + Control::Table(t) + if t.common.treat_as_char + && matches!(t.common.text_wrap, crate::model::shape::TextWrap::TopAndBottom) => + Some(hwpunit_to_px(t.common.height as i32, self.dpi) + + hwpunit_to_px(t.outer_margin_top as i32, self.dpi) + + hwpunit_to_px(t.outer_margin_bottom as i32, self.dpi)), + _ => None, + })) + .unwrap_or(0.0) + } else { + 0.0 + }; + let candidate_offset = st.current_zone_y_offset + vpos_zone_height + tac_band_extra + + st.current_zone_design_spacing_px / 2.0 + new_ds / 2.0; + + // [Task #853] 새 zone 이 현재 페이지 하단 가까이(여유 ≲ 헤더 띠 1개 높이)에서 시작하면 + // 그 zone 의 콘텐츠(헤더 띠 ~47px 또는 본문 줄들)가 body 하단을 넘어 렌더되므로 다음 + // 페이지로 넘긴다. (shortcut.hwp 3쪽~6쪽 — 다단 zone 다수 누적 시 잔여 콘텐츠가 + // 본문영역을 넘어 바닥 여백에 그려지던 결함) + let one_line = hwpunit_to_px(1500, self.dpi); + if candidate_offset > st.layout.available_body_height() - 4.0 * one_line { + st.push_new_page(); + // 새 페이지 첫 zone: 새 zone 디자인 spacing /2 만 (이전 zone 은 이전 페이지). + st.current_zone_y_offset = new_ds / 2.0; + } else { + st.current_zone_y_offset = candidate_offset; + } + st.current_zone_design_spacing_px = new_ds; st.current_column = 0; st.current_height = 0.0; st.on_first_multicolumn_page = true; diff --git a/tests/golden_svg/issue-267/ktx-toc-page.svg b/tests/golden_svg/issue-267/ktx-toc-page.svg index 50ccb95fd..6c81daa5f 100644 --- a/tests/golden_svg/issue-267/ktx-toc-page.svg +++ b/tests/golden_svg/issue-267/ktx-toc-page.svg @@ -23,8 +23,8 @@ - - + + diff --git a/tests/golden_svg/issue-617/exam-kor-page5.svg b/tests/golden_svg/issue-617/exam-kor-page5.svg index c763b55c8..bfbc7b457 100644 --- a/tests/golden_svg/issue-617/exam-kor-page5.svg +++ b/tests/golden_svg/issue-617/exam-kor-page5.svg @@ -13,14 +13,14 @@ -6 +6 - - - + + +