diff --git a/mydocs/plans/task_m05x_855.md b/mydocs/plans/task_m05x_855.md new file mode 100644 index 000000000..9f334b77d --- /dev/null +++ b/mydocs/plans/task_m05x_855.md @@ -0,0 +1,49 @@ +# 수행 계획서 — Task #855 + +## 대상 이슈 + +[#855] 21_언어_기출_편집가능본.hwp 14p 우측 단: Square-wrap 표 뒤 문단(pi=300) 렌더링 누락 + +## 현상 요약 + +`samples/21_언어_기출_편집가능본.hwp` 14페이지 오른쪽 단(`단 1`) 하단부가 렌더링되지 않음. + +`dump-pages -p 13` 결과 (단 1): +``` +단 1 (items=8, used=919.0px, hwp_used≈1219.3px, diff=-300.3px) + ... + PartialParagraph pi=299 lines=0..9 vpos=51892..66420 + Table pi=299 ci=0 3x2 22.7x220.7px wrap=Square tac=false + PartialParagraph pi=301 lines=0..1 vpos=90345..0 [vpos-reset@line1] +``` +- 문단 `pi=300` ("최근에는 기존의 법학방법론적 논의와…") 이 페이지 레이아웃 결과에서 **통째로 누락**. +- 페이지 15 첫 항목은 `pi=301 lines=1..22` → `pi=300` 은 다음 페이지로 넘어간 것도 아님 (완전 소실). +- `단 1` 누락 높이(약 300px)가 `pi=300` 분량(line seg 12개, 약 58mm)과 일치. + +## 1차 조사 (코드 미수정) + +- `pi=299` 에 `wrap=어울림(Square)`, `쪽나눔=RowBreak`, 크기 6.0×58.4mm 인 3×2 표가 문단 기준 위치(세로 오프셋 1.7mm)로 앵커되어 있음. +- 이 표는 `pi=299` 의 9개 라인(약 51mm)보다 길어서(58mm) 표 하단이 `pi=300` 의 첫 라인(vpos 68236)보다 약간 아래(≈68442)까지 내려옴. +- 라인 세그먼트 자체는 정상: `pi=300 ls[0]` 만 표를 피해 narrow(`sw=27581`), 나머지는 full(`sw=30184`). +- **추정 원인**: Square-wrap 개체가 앵커 문단보다 아래로 확장될 때, 레이아웃이 그 개체의 하단 y를 커서로 잡고 "그 y보다 위쪽 vpos 를 가진 문단"을 이미 배치된 것으로 간주해 건너뛰는 것으로 보임 → `pi=300`(시작 vpos 68236 < 표 하단 68442)이 스킵, `pi=301`(vpos 90345 > 68442)은 정상 배치. + +## 작업 범위 + +1. 레이아웃(`src/renderer/layout.rs` 등) 에서 Square/어울림 wrap 개체 처리 후 다음 문단 진입 로직 정밀 조사 — 어디서 `pi=300` 이 누락되는지 확정. +2. 원인 지점 수정 (개체 하단 y 와 무관하게 후속 문단은 정상적으로 큐에서 소비되도록). +3. 회귀 검증: `dump-pages -p 13/14`, `export-svg -p 13` 으로 `pi=300` 렌더링 확인. `pdf/21_언어_기출_편집가능본-2022.pdf` 14페이지와 시각 대조. +4. 다른 샘플 회귀 없음 확인 (`cargo test`, 주요 샘플 SVG diff 스팟체크). + +## 산출물 + +- 구현 계획서: `mydocs/plans/task_m05x_855_impl.md` +- 단계별 완료 보고서: `mydocs/working/task_m05x_855_stage{N}.md` +- 최종 보고서: `mydocs/report/task_m05x_855_report.md` + +## 브랜치 + +`local/task855` (from `local/devel`) + +--- + +승인해 주시면 구현 계획서 작성으로 넘어가겠습니다. diff --git a/mydocs/plans/task_m05x_855_impl.md b/mydocs/plans/task_m05x_855_impl.md new file mode 100644 index 000000000..1a329183a --- /dev/null +++ b/mydocs/plans/task_m05x_855_impl.md @@ -0,0 +1,58 @@ +# 구현 계획서 — Task #855 + +## 대상 이슈 + +[#855] 21_언어_기출_편집가능본.hwp 14p 우측 단: Square-wrap 표 뒤 문단(pi=300) 렌더링 누락 + +## 원인 분석 (확정) + +`src/renderer/pagination/engine.rs` 의 어울림(Square wrap) 오버랩 처리 (라인 289~323). + +- 앵커 문단 `pi=299` 의 표 컨트롤 검출 시 `wrap_around_cs = 3455`, `wrap_around_sw = 27581` 로 설정 (앵커 첫 LINE_SEG 값). +- 다음 문단 `pi=300` 의 LINE_SEG: + - `ls[0]`: cs=3455, sw=27581 — 표를 피해 들여쓰기된 첫 줄 (표와 같은 y) + - `ls[1..11]`: cs=852, sw=30184 — 표 아래, 본문 전체 폭 +- 현재 판정 `para_cs == wrap_around_cs && para_sw == wrap_around_sw` 는 **첫 LINE_SEG만** 검사 → `pi=300` 전체를 "표 옆에 나란히 배치되는 0-높이 문단"으로 간주 → `continue` 로 높이 소비 없이 `WrapAroundPara` 에만 등록 → `pi=300` 의 줄 12개가 페이지 흐름에서 사라짐. +- 결과: `pi=300` 통째 누락, `단 1` 높이 약 300px 부족. + +`WrapAroundPara` 메커니즘은 본래 좁고 긴 어울림 표 **옆 공간을 채우는, 전부 표 옆에 들어가는 문단**(주로 빈 리턴 문단)을 위한 것. `pi=300` 처럼 첫 줄만 표 옆이고 나머지가 표 아래로 흐르는 문단은 일반 텍스트 배치(`paginate_text_lines`)로 처리되어야 함 — LINE_SEG 의 cs/sw 가 이미 wrap 형상을 인코딩하므로 레이아웃은 그대로 정상 렌더. + +## 구현 단계 + +### Stage 1 — 어울림 문단 판정 조건 보정 + +`engine.rs` 어울림 오버랩 블록(라인 ~304 조건문) 수정: + +- "전부 표 옆 문단" 판정을 **모든 (비어있지 않은) LINE_SEG 가 wrap zone(cs/sw) 과 일치**할 때로 한정. + - 구체: `para.line_segs.iter().all(|s| s.column_start == wrap_around_cs && s.segment_width as i32 == wrap_around_sw)` 를 추가 조건으로 요구 (빈 문단·`sw0_match` 경로는 기존 유지). + - 혹은 동치로 `para.line_segs.last()` 도 wrap zone 과 일치하는지 검사. +- 일치하지 않으면(= 일부 줄만 표 옆) `continue` 하지 않고 `wrap_around_cs/sw` 리셋 후 일반 텍스트 배치로 폴백 (현 `else` 분기와 동일 동작). +- `wrap_around_any_seg` (어울림 그림 any-seg 경로) 도 동일 원칙 적용 검토 — any-seg 가 true 여도 "전부 일치"가 아니면 폴백. + +### Stage 2 — 회귀 검증 (대상 샘플) + +- `cargo build --release` +- `rhwp dump-pages samples/21_언어_기출_편집가능본.hwp -p 13` → `단 1` 에 `pi=300` 항목이 정상 높이로 등장, `pi=301` 위치 정상. +- `rhwp dump-pages -p 14` → 페이지 15 첫 항목 변화 확인. +- `rhwp export-svg samples/21_언어_기출_편집가능본.hwp -p 13` → `pi=300` 본문 렌더링 + 표 옆 첫 줄 들여쓰기 확인. +- `pdf/21_언어_기출_편집가능본-2022.pdf` 14페이지와 시각 대조. + +### Stage 3 — 전체 회귀 + 마무리 + +- `cargo test` +- 어울림 표/그림이 있는 다른 샘플 몇 개 SVG 스팟체크 (회귀 없음 확인) — 예: 기존 `re_sample` 및 어울림 관련 테스트. +- `clippy` 통과 확인. +- 최종 보고서 `mydocs/report/task_m05x_855_report.md` 작성. + +## 영향 범위 + +- 수정 파일: `src/renderer/pagination/engine.rs` (어울림 오버랩 판정 1개 블록). +- 레이아웃·typeset 변경 없음. + +## 리스크 + +- "전부 일치" 로 좁힐 때, 기존에 `WrapAroundPara` 로 처리되던 정상 케이스(전부 표 옆 빈 문단)는 모든 seg 가 wrap zone 과 일치하므로 영향 없음. 다만 첫 줄만 표 옆이고 나머지가 본문 폭인 케이스가 다른 샘플에도 있을 수 있어 Stage 3 회귀 확인 필수. + +--- + +승인해 주시면 Stage 1 구현 시작하겠습니다. diff --git a/mydocs/report/task_m05x_855_report.md b/mydocs/report/task_m05x_855_report.md new file mode 100644 index 000000000..577f6e0fd --- /dev/null +++ b/mydocs/report/task_m05x_855_report.md @@ -0,0 +1,45 @@ +# 최종 결과 보고서 — Task #855 + +## 이슈 + +[#855] 21_언어_기출_편집가능본.hwp 14p 우측 단: Square-wrap 표 뒤 문단(pi=300) 렌더링 누락 + +## 증상 + +`samples/21_언어_기출_편집가능본.hwp` 14페이지 오른쪽 단에서, `[A]` 묶음 박스(어울림 배치 3×2 표) **아래쪽 본문**("최근에는 기존의 법학방법론적 논의와 …" — `pi=300`)이 렌더링되지 않음. 페이지 흐름에서 문단 전체가 소실 (다음 페이지로 넘어간 것도 아님). + +## 원인 + +`src/renderer/typeset.rs` 의 어울림(Square wrap) 표 옆 문단 흡수 로직. + +어울림 표 anchor 의 wrap zone(`column_start`, `segment_width`)을 등록한 뒤, 후속 문단이 그 zone 과 일치하는지 검사할 때 **첫 LINE_SEG 만** 비교했다. `pi=300` 은 12개 LINE_SEG 중 첫 줄(`ls[0]`)만 표를 피해 들여쓰기되어 wrap zone(cs=3455, sw=27581)과 일치하고, 나머지 11줄(`ls[1..11]`)은 표 아래 본문 전체 폭(cs=852, sw=30184)이다. 그런데 첫 LINE_SEG 일치만으로 문단 **전체**를 "표 옆에 나란히 들어가는 0-높이 문단"으로 간주하여 `current_column_wrap_around_paras` 에만 기록하고 `continue` → 페이지 높이를 소비하지 않고 흐름에서 제외 → `pi=300` 누락. + +`WrapAroundPara` 흡수 메커니즘은 본래 좁고 긴 어울림 표 옆 공간을 채우는, **전체가 표 옆에 들어가는** 문단(주로 빈 ↵ 표시 문단)을 위한 것이었다. + +## 수정 + +`src/renderer/typeset.rs` — Table anchor 흡수 분기에서, 후속 문단의 **마지막 LINE_SEG 도** wrap zone(cs, sw)과 일치할 때(또는 빈 문단일 때)만 0-높이 흡수하도록 조건을 강화. 불일치 시(= 일부 줄만 표 옆) wrap zone 을 종료하고 일반 텍스트 배치로 폴백한다. LINE_SEG 의 cs/sw 가 이미 wrap 형상을 인코딩하므로, layout 은 첫 줄을 표 옆 들여쓰기로, 나머지 줄을 표 아래 전폭으로 정상 렌더한다. + +Picture anchor 흡수 분기(`wrap_anchors` 등록 → FullParagraph 통과)는 영향 없음. + +- 수정 파일: `src/renderer/typeset.rs` (1개 분기, 약 +13줄) +- 레이아웃·문서코어·HWP3 파서 변경 없음 + +### 미처리(후속 정합 항목) + +`src/renderer/pagination/engine.rs` 의 `paginate_with_measured_opts` 에도 동일 로직(주석에 "engine.rs:288-320 동일 시멘틱")이 존재한다. 이 경로는 `RHWP_USE_PAGINATOR=1` 일 때만 동작하는 fallback 이며 기본값(TypesetEngine)에는 영향이 없어 본 타스크에서는 수정하지 않았다. 두 구현 정합이 필요하면 별도 타스크로 처리한다. + +## 검증 + +| 항목 | 결과 | +|------|------| +| `cargo build --release` | 성공 | +| `cargo test --release` | 전체 통과 (1232 + 통합 테스트, 0 failed) | +| `cargo clippy --release` | 경고 0건 | +| `dump-pages -p 13` | 수정 전 `단 1` items=8 / `pi=300` 누락 / diff=-300.3px → 수정 후 items=10 / `pi=300` `FullParagraph h=180.2` 등장 / diff=-15.1px | +| `export-svg -p 13` 시각 검증 | `[A]` 박스 아래 `pi=300` 본문 정상 렌더, `pdf/...-2022.pdf` 14p 단 구조와 일치 | +| 샘플 SVG 스팟체크 | `samples/`·`samples/basic/`·`samples/hwpx/` 전부 패닉/오류 없음 | + +## 결론 + +의도한 버그(어울림 표 아래 문단 누락) 해결. 회귀 없음. merge 가능. diff --git a/mydocs/working/task_m05x_855_stage1.md b/mydocs/working/task_m05x_855_stage1.md new file mode 100644 index 000000000..9964916d9 --- /dev/null +++ b/mydocs/working/task_m05x_855_stage1.md @@ -0,0 +1,29 @@ +# Stage 1 완료 보고서 — Task #855 + +## 작업 내용 + +어울림(Square wrap) 표 옆 문단 흡수 판정 보정. + +### 계획 대비 변경점 + +구현 계획서에는 수정 대상을 `src/renderer/pagination/engine.rs` 로 적었으나, 실제 활성 페이지네이션 경로는 `TypesetEngine`(`src/renderer/typeset.rs`) 임을 조사 중 확인했다 (`RHWP_USE_PAGINATOR=1` 일 때만 `engine.rs` 의 `paginate_with_measured_opts` 가 fallback 으로 사용됨; 기본값은 `TypesetEngine::typeset_section`). 따라서 수정은 `src/renderer/typeset.rs` 에 적용했다. `engine.rs` 의 동일 로직(주석에 "engine.rs:288-320 동일 시멘틱" 명시)은 fallback 경로이며 본 타스크 범위에서는 손대지 않았다 (후속 정합 항목으로 보고서에 기록). + +## 수정 (`src/renderer/typeset.rs`) + +어울림 표 anchor 의 wrap zone (cs, sw) 과 후속 문단 매칭 시, Table anchor 흡수 분기(`current_column_wrap_around_paras` 등록 + `continue`)에서: + +- 기존: 후속 문단의 **첫 LINE_SEG** 만 wrap zone 과 일치하면 문단 전체를 0-높이로 흡수 → 첫 줄만 표 옆이고 나머지 줄이 본문 전체 폭으로 흐르는 문단(`pi=300`)이 통째로 페이지 흐름에서 누락. +- 수정: **마지막 LINE_SEG 도** wrap zone (cs, sw) 과 일치할 때(또는 빈 문단일 때)만 흡수. 불일치 시 wrap zone 을 종료하고 일반 텍스트 배치로 폴백 — LINE_SEG 의 cs/sw 가 이미 wrap 형상(첫 줄 들여쓰기, 나머지 전폭)을 인코딩하므로 layout 이 첫 줄을 표 옆에, 나머지를 표 아래에 정상 렌더한다. + +Picture anchor 분기(`wrap_anchors` 등록 + FullParagraph 통과)는 변경하지 않았다. + +## 검증 (1차) + +- `cargo build --release` 성공. +- `rhwp dump-pages samples/21_언어_기출_편집가능본.hwp -p 13`: + - 수정 전: `단 1` items=8, `pi=300` 누락, diff=-300.3px + - 수정 후: `단 1` items=10, `pi=300` (`FullParagraph h=180.2`) 정상 등장, diff=-15.1px + +## 다음 단계 + +Stage 2 — SVG/PDF 시각 검증. diff --git a/mydocs/working/task_m05x_855_stage2.md b/mydocs/working/task_m05x_855_stage2.md new file mode 100644 index 000000000..ca64bfe6d --- /dev/null +++ b/mydocs/working/task_m05x_855_stage2.md @@ -0,0 +1,17 @@ +# Stage 2 완료 보고서 — Task #855 + +## 시각 검증 + +`samples/21_언어_기출_편집가능본.hwp` 14페이지 (global_idx=13). + +- `rhwp export-svg ... -p 13` → `output/svg/21_언어_기출_편집가능본_014.svg` 생성. +- `rsvg-convert` 로 PNG 변환하여 육안 확인: + - 우측 단의 `[A]` 묶음 박스(어울림 3×2 표) 아래로 `pi=300` 본문("최근에는 기존의 법학방법론적 논의와 …")이 정상 렌더됨. + - 박스 옆 첫 줄은 들여쓰기, 박스 아래 줄들은 본문 전체 폭으로 흐름 — `pdf/21_언어_기출_편집가능본-2022.pdf` 14페이지와 단 구조 일치. +- SVG 내 `pi=300` 본문 글리프(`최`, `극`, `복` 등) 존재 확인. + +수정 전에는 해당 영역이 비어 있었다(텍스트 글리프 0개). + +## 다음 단계 + +Stage 3 — 전체 회귀(`cargo test`, `cargo clippy`, 샘플 SVG 스팟체크). diff --git a/mydocs/working/task_m05x_855_stage3.md b/mydocs/working/task_m05x_855_stage3.md new file mode 100644 index 000000000..e4c623e3c --- /dev/null +++ b/mydocs/working/task_m05x_855_stage3.md @@ -0,0 +1,15 @@ +# Stage 3 완료 보고서 — Task #855 + +## 전체 회귀 + +- `cargo test --release`: 전체 통과 (1232 + 통합 테스트 모두 `ok`, 0 failed). 사전 존재하던 "unused Result" 경고 2건은 본 수정과 무관 (변경 코드에 Result 없음). +- `cargo clippy --release`: 경고 0건. +- 샘플 SVG 스팟체크: `samples/*.hwp`, `samples/basic/*.hwp`, `samples/hwpx/*.hwpx` 전부 `export-svg` 패닉/오류 없이 정상 출력. + +## 결론 + +수정으로 인한 회귀 없음. Task #855 의도 충족. + +## 다음 단계 + +최종 보고서 작성 → 승인 요청. diff --git a/src/renderer/typeset.rs b/src/renderer/typeset.rs index f822f34fb..8cb37a2af 100644 --- a/src/renderer/typeset.rs +++ b/src/renderer/typeset.rs @@ -622,15 +622,30 @@ impl TypesetEngine { }, ); } else { - // Table anchor: 어울림 문단을 표 옆에 기록 + height 소비 없음 - st.current_column_wrap_around_paras.push( - crate::renderer::pagination::WrapAroundPara { - para_index: para_idx, - table_para_index: st.wrap_around_table_para, - has_text: !is_empty_para, - } - ); - continue; + // Table anchor: 어울림 문단을 표 옆에 기록 + height 소비 없음. + // [Task #855] 단, 첫 줄만 표 옆이고 나머지 줄이 본문 전체 폭으로 + // 흐르는 문단(= 마지막 LINE_SEG 가 wrap zone cs/sw 와 불일치)은 + // 0-높이 흡수 대상이 아니다. 첫 LINE_SEG 만 보고 흡수하면 그런 문단이 + // 통째로 페이지 흐름에서 누락된다. 이 경우 wrap zone 을 종료하고 + // 일반 텍스트 배치로 폴백한다 (LINE_SEG cs/sw 가 이미 wrap 형상을 + // 인코딩하므로 layout 이 첫 줄을 표 옆에, 나머지를 표 아래에 렌더). + let last_seg_match = para.line_segs.last().map(|s| + s.column_start == st.wrap_around_cs && s.segment_width as i32 == st.wrap_around_sw + ).unwrap_or(false); + if last_seg_match || is_empty_para { + st.current_column_wrap_around_paras.push( + crate::renderer::pagination::WrapAroundPara { + para_index: para_idx, + table_para_index: st.wrap_around_table_para, + has_text: !is_empty_para, + } + ); + continue; + } + st.wrap_around_cs = -1; + st.wrap_around_sw = -1; + st.wrap_around_any_seg = false; + // fall through → 일반 paragraph 배치 } } else { // 매칭 실패 → wrap zone 종료, 정상 처리 진행