Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions mydocs/plans/task_m05x_855.md
Original file line number Diff line number Diff line change
@@ -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`)

---

승인해 주시면 구현 계획서 작성으로 넘어가겠습니다.
58 changes: 58 additions & 0 deletions mydocs/plans/task_m05x_855_impl.md
Original file line number Diff line number Diff line change
@@ -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 구현 시작하겠습니다.
45 changes: 45 additions & 0 deletions mydocs/report/task_m05x_855_report.md
Original file line number Diff line number Diff line change
@@ -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 가능.
29 changes: 29 additions & 0 deletions mydocs/working/task_m05x_855_stage1.md
Original file line number Diff line number Diff line change
@@ -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 시각 검증.
17 changes: 17 additions & 0 deletions mydocs/working/task_m05x_855_stage2.md
Original file line number Diff line number Diff line change
@@ -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 스팟체크).
15 changes: 15 additions & 0 deletions mydocs/working/task_m05x_855_stage3.md
Original file line number Diff line number Diff line change
@@ -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 의도 충족.

## 다음 단계

최종 보고서 작성 → 승인 요청.
33 changes: 24 additions & 9 deletions src/renderer/typeset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 종료, 정상 처리 진행
Expand Down
Loading