fix: 복수 제목행 반복 시 2행 이상 출력 정정 (closes #594)#601
fix: 복수 제목행 반복 시 2행 이상 출력 정정 (closes #594)#601oksure wants to merge 2 commits intoedwardkim:develfrom
Conversation
기존 코드는 repeat_header 시 행 0 만 제목행으로 반복했으나, 제목행이 2개 이상인 표에서 두 번째 이후 제목행이 누락되었음. 수정: is_header 셀이 존재하는 모든 행을 수집하여 반복 렌더링.
There was a problem hiding this comment.
Pull request overview
Fixes pagination rendering for split tables when repeat_header is enabled and the table header consists of multiple rows, by repeating all rows that contain is_header cells (instead of hardcoding row 0 only).
Changes:
- Collects all rows containing
is_headercells intoheader_rowsfor continuation-page rendering. - Renders repeated header rows on continuation pages and avoids duplicate row insertion.
- Updates cell-inclusion range logic to account for repeated multi-row headers.
Comments suppressed due to low confidence (1)
src/renderer/layout/table_partial.rs:175
header_rows.contains(...)is used inside the per-row loop and again per-cell during rendering. For large tables, this introduces avoidable O(header_rows.len()) scans in hot paths. Since you already build aseenboolean vector, consider retaining a boolean membership structure (or aHashSet) for O(1) header-row membership checks while still keepingheader_rowsfor ordering.
let mut seen = vec![false; row_count];
for c in &table.cells {
if c.is_header && (c.row as usize) < row_count && !seen[c.row as usize] {
seen[c.row as usize] = true;
header_rows.push(c.row as usize);
}
}
header_rows.sort_unstable();
}
let mut render_rows: Vec<usize> = Vec::new();
render_rows.extend_from_slice(&header_rows);
for r in start_row..end_row.min(row_count) {
if !header_rows.contains(&r) {
render_rows.push(r);
}
}
// 렌더링 영역의 행별 y 위치 계산 (0부터 시작)
let mut render_row_y: Vec<f64> = Vec::new(); // 각 render_rows 항목의 시작 y
let mut y_accum = 0.0;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let mut header_rows: Vec<usize> = Vec::new(); | ||
| if is_continuation && table.repeat_header && start_row > 0 { | ||
| let mut seen = vec![false; row_count]; | ||
| for c in &table.cells { | ||
| if c.is_header && (c.row as usize) < row_count && !seen[c.row as usize] { | ||
| seen[c.row as usize] = true; | ||
| header_rows.push(c.row as usize); | ||
| } | ||
| } | ||
| header_rows.sort_unstable(); | ||
| } | ||
| let mut render_rows: Vec<usize> = Vec::new(); | ||
| render_rows.extend_from_slice(&header_rows); | ||
| for r in start_row..end_row.min(row_count) { | ||
| render_rows.push(r); | ||
| if !header_rows.contains(&r) { | ||
| render_rows.push(r); | ||
| } |
| // is_continuation && repeat_header → is_header 셀이 있는 모든 행을 반복 | ||
| let mut header_rows: Vec<usize> = Vec::new(); | ||
| if is_continuation && table.repeat_header && start_row > 0 { | ||
| let mut seen = vec![false; row_count]; | ||
| for c in &table.cells { | ||
| if c.is_header && (c.row as usize) < row_count && !seen[c.row as usize] { | ||
| seen[c.row as usize] = true; | ||
| header_rows.push(c.row as usize); | ||
| } | ||
| } | ||
| header_rows.sort_unstable(); | ||
| } | ||
| let mut render_rows: Vec<usize> = Vec::new(); | ||
| render_rows.extend_from_slice(&header_rows); | ||
| for r in start_row..end_row.min(row_count) { |
| // is_continuation && repeat_header → is_header 셀이 있는 모든 행을 반복 | ||
| let mut header_rows: Vec<usize> = Vec::new(); | ||
| if is_continuation && table.repeat_header && start_row > 0 { | ||
| let mut seen = vec![false; row_count]; | ||
| for c in &table.cells { | ||
| if c.is_header && (c.row as usize) < row_count && !seen[c.row as usize] { | ||
| seen[c.row as usize] = true; | ||
| header_rows.push(c.row as usize); | ||
| } | ||
| } | ||
| header_rows.sort_unstable(); | ||
| } | ||
| let mut render_rows: Vec<usize> = Vec::new(); | ||
| render_rows.extend_from_slice(&header_rows); | ||
| for r in start_row..end_row.min(row_count) { | ||
| render_rows.push(r); | ||
| if !header_rows.contains(&r) { | ||
| render_rows.push(r); | ||
| } | ||
| } |
- header_rows 수집을 r < start_row 조건으로 제한하여, 데이터 범위 내 is_header 행이 의도치 않게 상단으로 재배치되는 문제 방지 - render_rows 루프에서 중복 제거 조건도 제거 (header_rows ⊂ [0, start_row)) - 페이지네이션 높이 예약: MeasuredTable 에 header_row_flags 추가 필요 (후속 작업)
|
Addressed review feedback in 80db71a:
|
v0.7.9 후속 patch 사이클 (5/4 ~ 5/6). ## 신규 기능 - **CLI 바이너리 릴리즈** (Issue #608/#612, @almet 의 요청) - 4 플랫폼 GitHub Release 자산 첨부 (Linux x86_64 / macOS x86_64+aarch64 / Windows x86_64) + SHA-256 체크섬 - **PNG raster backend** (PR #599, @seo-rii) — render P4 단계 - native Skia 기반 PageLayerTree → PNG export, native-skia feature gate - **AI 파이프라인 + VLM 연동 도입** (메인테이너 후속 정정): - --vlm-target claude (1568 longest edge / 1.15 MP, Claude Vision 정합) - --scale / --max-dimension (자동 scale 계산) - export-png CLI 명령 + 매뉴얼 (한글 + 영문 dual) - 한글 폰트 fallback chain + char 단위 fallback (공백 두부 정정) + --font-path 동적 로딩 ## 외부 PR cherry-pick (13 PR / 7 컨트리뷰터) - @planet6897 / Jaeook Ryu (협업): PR #587/#589/#561/#564/#570/#575/ #580/#584/#592/#593/#567 - @oksure (Hyunwoo Park): PR #600 (closes #513) - @seo-rii: PR #599 (refs #536) - @cskwork / @johndoekim / @nameofSEOKWONHONG / @jangster77 — 사이클 누적 ## 메인테이너 정정 Skia 폰트 영역 5개 정정 (한글 fallback / font-path / char-fallback / VLM 옵션 / export-png CLI). ## 인프라 - CI 빌드 안정성 (Cargo.toml [[example]] required-features) - 광범위 페이지네이션 회귀 sweep 도구 (164 fixture / 1,614 페이지 자동) ## 후속 이슈 - #613 (VLM 프리셋 확장) - #614 (DPI 메타데이터) - #615 (pua_oldhangul.rs U+F53A 한컴 정합) - #598 (rhwp-studio 각주 삭제, 외부 컨트리뷰터 공개) ## 잔여 PR (v0.7.11 후속 patch) PR #601, #602 (@oksure) / PR #607 (@dicebattle) / PR #609 (@jangster77, Task #604) / PR #611 (@kihyunnn). 상세: CHANGELOG.md (한글) / CHANGELOG_EN.md (영문).
- @oksure (Hyunwoo Park) Task #594 복수 제목행 반복 정정 PR 검토 - 단일 파일 (table_partial.rs +22/-12), 2 commits (본질 + Copilot review address) - 본 환경 검증: cargo test 1141 passed / clippy 0 / svg_snapshot 6/6 / 광범위 sweep 회귀 0 - 정량 측정: PR 본문 명시 권위 샘플 (synam-001 35 + aift 77 페이지) 모두 byte-identical (회귀 0) - Copilot review 4 코멘트 자체 검토 응답 정합 (header_rows scope + seen O(1) + pagination 후속 영역) - Issue #594 첨부 테스트.hwp 본 환경 미존재 → 작업지시자 시각 판정 게이트 권고 - 옵션 A (cherry-pick + 시각 판정) + 옵션 B (후속 권유) 결합 권장 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 13.1 핀셋 cherry-pick (0059557, author Hyunwoo Park 보존) - 13.2 결정적 재검증 (1141 passed / clippy 0 / WASM 4,588,023 bytes) - 13.3 작업지시자 안내 영역 (aift.hwp 페이지 수 47/77 차이 + 시각적 개선 미발현 본 환경 fixture 영역) - 13.4 권장 처리 방향 정합 (옵션 A-1 권위 샘플 도입 / A-2 그대로 머지 + 후속 / A-3 close + 후속 PR 권유) - 13.5 WASM 산출물 정합 (PR #642 baseline +705 bytes) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 47 페이지 (page_num=41) = global_idx=46 영역 dump-pages 분석 - pi=579/581/584 모두 단일 제목행 영역 → 본 PR fix 분기 미발현 (회귀 0 정합) - examples/inspect_pr601.rs 진단 스크립트 신규 (aift.hwp is_header sweep) - 다중 제목행 표 발견: s2 pi=147 (7×4) + s2 pi=745 (9×14) — 분할 미발현 - 결론: 본 환경 aift.hwp 다중 제목행 + 분할 표 발현 영역 부재 → 시각적 개선 미발현 정합 - 권장: 옵션 A-1 (테스트.hwp 도입 후 재시각 판정) / A-2 (회귀 0 입증으로 머지 + 후속) / A-3 (close + 후속 PR 권유) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
검토 + 핀셋 cherry-pick 머지 완료. 감사합니다. 처리 결과 — 옵션 A-2 진행
결정적 재검증 (본 환경)
광범위 페이지네이션 회귀 sweep
정량 측정 (PR 본문 명시 권위 샘플)
→ 두 샘플의 표는 단일 제목행 영역 으로 본 PR fix 의 분기 미발현 (회귀 0 정합 입증). 시각 발현 영역 정밀 분석 (
|
| 영역 | 개수 |
|---|---|
| is_header 부재 표 | 85 개 |
| 단일 제목행 표 | 3 개 (47 페이지 영역의 pi=579 / pi=581 / pi=584 모두) |
| 다중 제목행 표 | 2 개 (s2 pi=147 7×4 + s2 pi=745 9×14) — 한 페이지에 완전히 들어감, 분할 미발현 |
→ 본 환경 aift.hwp 의 다중 제목행 + 분할 표 발현 영역 부재. 본 PR 본문 명시 "synam-001.hwp p15 (분할 표): 정상 렌더링 확인" + "aift.hwp (대형 표 다수): export-pdf 에러 없음" 은 회귀 0 입증 영역으로 정합하지만, 본 PR fix 의 시각적 효과 발현은 Issue #594 첨부 테스트.hwp 영역의 권위 케이스에서만 가능.
Copilot review 4 코멘트 자체 검토 응답 정합
- ✅ baseline/line_height/line_spacing 역공학 #1 header_rows scope (
r < start_row가드) —80db71a응답 정합 ⚠️ 편집 파이프라인 파생 갭 수정 #2 pagination height (MeasuredTable.header_row_flags) — 후속 task 영역⚠️ 셀 내 TAC 이미지 수직 배치 버그 수정 #3 regression test (Issue HWP->PDF 변환 시 페이지가 넘어가는 표에서 복수 제목행 반복 시 제목 출력이 깨지는 문제 #594 첨부테스트.hwp본 환경 미존재) — 후속 영역 (메인테이너 권위 샘플 도입)- ✅ 비-TAC 그림(어울림 배치) 높이가 후속 요소 y에 미반영 #4 O(N) 최적화 (
seenboolean vec) — 본질 영역에서 이미 적용 정합
본 PR 의 본질 — 옵션 A-2 진행 정합
본 PR 의 처리 본질에서 가장 우수한 점:
- 본질 진단 정확 —
table_partial.rs:154의render_rows.push(0)단일 행 하드코딩 영역 정확 식별 - HWP IR 표준 직접 사용 —
is_header셀 동적 수집, 휴리스틱 미도입 - 케이스별 명시 가드 — 4 분기 (
is_continuation+repeat_header+start_row > 0+r < start_row) 로 회귀 위험 좁음 - 회귀 0 입증 — 본 환경 권위 샘플 (synam-001 35 + aift 77 페이지) 모두 byte-identical
- Copilot review 자체 검토 응답 정합 — 본질 영역 + 후속 영역 분리 명료
- 활발한 컨트리뷰터 영역 (oksure 5번째 PR) — 차분/사실 중심 톤 + Copilot review 응답 정합
후속 영역 (별도 task 후보)
MeasuredTable.header_row_flags추가 — 다중 제목행 높이 예약 (Copilot review 편집 파이프라인 파생 갭 수정 #2)- 회귀 테스트 영역 — Issue HWP->PDF 변환 시 페이지가 넘어가는 표에서 복수 제목행 반복 시 제목 출력이 깨지는 문제 #594 첨부
테스트.hwp본 환경 도입 + integration test - 본 환경 권위 샘플 영역 — 다중 제목행 + 분할 표 발현 fixture 도입
수고하셨습니다.
PR #601 처리 (옵션 A-2 정합): - mydocs/pr/archives/pr_601_report.md 신규 (처리 보고서) - mydocs/pr/pr_601_review.md → mydocs/pr/archives/pr_601_review.md 이동 - mydocs/orders/20260507.md 갱신 (PR #601 항목 + Issue #652 신규 영역) Issue #652 권위 자료 영역 (HWP5 vpos 부풉 결함): - samples/hwpx/aift.hwpx (한컴 2020 변환본, 74 페이지 한컴 정합) - samples/hwpx/aift-2020.pdf (한컴 2010/2020 PDF 권위 정답지, 74 페이지) - 본 환경 결함 영역 좁힘: HWP5 만 +3 페이지 부풉 (HWPX 정합) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
문제
페이지를 넘어가는 표에서 제목행 반복이 적용될 때, 제목행이 2개 이상 행으로 구성된 경우 첫 번째 제목행만 반복되고 나머지는 누락됩니다.
원인:
table_partial.rs에서render_rows.push(0)으로 행 0만 하드코딩.수정 내용
is_header셀이 있는 모든 행을 수집하여header_rows벡터 구성검증
samples/synam-001.hwpp15 (분할 표): 정상 렌더링 확인samples/aift.hwp(41 페이지, 대형 표 다수): export-pdf 에러 없음참고: 첨부된 테스트.hwp 는 로컬에 없어 직접 검증하지 못했으나, 코드 변경은 리포터가 지적한
table_partial.rs:154의 단일 행 하드코딩을 정확히 수정합니다.피드백 있으시면 말씀해주세요.