Skip to content

fix: HWP5 line_segs 누락 paragraph 겹침 해소 — composer word wrap 합성 (closes #994)#997

Closed
jangster77 wants to merge 7 commits into
edwardkim:develfrom
jangster77:local/task994
Closed

fix: HWP5 line_segs 누락 paragraph 겹침 해소 — composer word wrap 합성 (closes #994)#997
jangster77 wants to merge 7 commits into
edwardkim:develfrom
jangster77:local/task994

Conversation

@jangster77

Copy link
Copy Markdown
Collaborator

Summary

Root cause

HWP3 → HWP5 변환 시 일부 long-text paragraph 의 `PARA_LINE_SEG` 레코드 누락:

Paragraph 타입 HWP3 line_segs HWP5 line_segs
`󰏅` (PUA bullet, long text) 있음 누락
` ◦` (regular bullet) 있음 있음

기존 `compose_lines` fallback (composer.rs:301-326) 이 단일 ComposedLine 생성 →
layout 이 wrap 없이 한 y 좌표에 모든 chars 그림 → 시각 겹침.

영향 paragraph: HWP5 sample16 에서 59 개.

Fix (G4-final, 3차 반복)

src/renderer/composer.rs 의 `compose_lines` fallback:

```rust
fn compose_lines(para: &Paragraph) -> Vec {
if para.line_segs.is_empty() {
if para.text.is_empty() { return Vec::new(); }
let default_style_id = ...;

    let chars: Vec<char> = para.text.chars().collect();
    const CHARS_PER_LINE: usize = 35;
    let mut lines = Vec::new();
    let mut offset = 0;
    while offset < chars.len() {
        let max_end = (offset + CHARS_PER_LINE).min(chars.len());
        // [Task #994] Word boundary 분할 — Justify spacing 부풀림 회피
        let mut end = max_end;
        if end < chars.len() {
            let min_acceptable = offset + (CHARS_PER_LINE / 2);
            for i in (min_acceptable..max_end).rev() {
                if chars[i] == ' ' || chars[i] == '\\t' {
                    end = i + 1; break;
                }
            }
        }
        let is_last_line = end >= chars.len();
        lines.push(ComposedLine {
            runs: ...,
            line_height: 400,
            // [Task #994] non-last synth line: Justify 비활성화
            has_line_break: !is_last_line,
            char_start: offset,
            ..
        });
        offset = end;
    }
    return lines;
}
// ... 기존 line_segs 처리 ...

}
```

3차 반복 시도 정리

시도 영향 결과
1차 G1 (paragraph_layout fallback line_height) 효과 없음 — 실제 path 미사용 폐기
2차 G4-wide (char-count 35 분할) 겹침 해소 but Justify 부풀림 부분
3차 G4-final (word boundary + has_line_break) 겹침 해소 + 자연 정렬 본 PR

Verification

  • ✅ cargo test --release --lib: 1297 passed, 0 failed
  • ✅ cargo fmt --check 통과
  • ✅ 240 sample 페이지 수 변동: 타깃 1건만 (62→67) + 신규 1건 (hy-001)
  • ✅ HWP5 sample16 page 19~24 시각: 작업지시자 판정 통과
  • ✅ Editor 기능 (parser 미변경): 영향 없음

Residual / 후속

  • Page count 차이 (별도 issue 예정): HWP3 64 vs HWP5 (post G4) 67 vs HWPX 자동보정 69 — paragraph height 누적 차이 별도 root cause
  • HWPX 변종 영향 가능 (parser path 동일) — 본 PR 의 scope 외

Test plan

  • cargo test --release 통과 확인
  • cargo fmt --check 통과 확인
  • HWP5 sample16-hwp5.hwp page 19~24 시각 정합 확인 (겹침 해소)
  • HWP3 sample16.hwp 등 line_segs 보유 sample 영향 없음 확인
  • Editor 기능 (insert_text/save/cursor) 동작 확인

🤖 Generated with Claude Code

… wrap 합성

Root cause:
HWP3 → HWP5 변환 시 일부 long-text paragraph (sample16 의 󰏅 PUA bullet 59개) 의
PARA_LINE_SEG 레코드 누락. composer 의 compose_lines fallback 이 단일 ComposedLine
생성 → layout 이 wrap 없이 한 y 좌표에 모든 chars 그림 → 시각 겹침
(page 19~24 영향).

Fix (G4-final, 3차 반복):
src/renderer/composer.rs 의 compose_lines fallback (line_segs.is_empty())
에서 paragraph 를 다수 ComposedLine 으로 분할.

1. Word boundary 분할 (~35 chars/line 한도, 공백 후 break) — mid-word 분할 회피
2. has_line_break=true 로 non-last synth line marking → Justify 정렬 비활성화
   (chars 사이 spacing 부풀림 회피)

이전 시도 vs 본 fix:
- 1차 (G1, paragraph_layout fallback line_height 보정): 효과 없음 — 실제 paragraph
  가 layout_inline_table_paragraph 미사용
- 2차 (G4-wide, 35 chars char-count 분할): 겹침 해소했으나 Justify 가 chars
  spacing 부풀림
- 3차 (본 PR): word boundary + has_line_break — 자연스러운 정렬

검증:
- cargo test --release --lib: 1297 passed, 0 failed ✓
- cargo fmt --check: 통과
- 240 sample 페이지 수: 타깃 1건 (62→67), 회귀 0
- HWP5 sample16 page 19~24 시각: 작업지시자 판정 통과
- Editor 기능 (parser 미변경): 영향 없음

영향 범위:
- composer 의 line_segs 누락 case 만 적용 — HWP3 / HWPX (line_segs 있음) 미영향
- HWP5 변환본의 long-text paragraph 자연 렌더링

후속 (별도 issue 예정):
- Page count 차이 (HWP3 64 vs HWP5 67 vs HWPX 자동보정 69) — paragraph height
  누적 차이 별도 root cause

closes edwardkim#994

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jangster77

Copy link
Copy Markdown
Collaborator Author

관련 후속 issue 등록: #998 — HWP3/HWP5/HWPX sample16 페이지 수 차이 (64 vs 67 vs 69) — paragraph height 누적 차이 별도 root cause.

edwardkim added a commit that referenced this pull request May 19, 2026
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
edwardkim added a commit that referenced this pull request May 19, 2026
@jangster77 — HWP3→HWP5 변환 시 일부 long-text paragraph (sample16 󰏅 PUA
bullet 59개) PARA_LINE_SEG 누락 → compose_lines fallback 단일 ComposedLine
→ 시각 겹침 (page 19~24). word-boundary 분할 + has_line_break=true 로 해소.

옵션 A: 본질 커밋 de21afa cherry-pick (작성자 Taesup Jang 보존, 충돌 없음).
검증: cargo test 1307 + clippy -D + fmt 0. sweep (BEFORE devel ↔ AFTER):
검토 쟁점 B(hy-001 HWPX/HWP5 표 셀 경로 — Task #671 가드 충돌 우려) diff=0,
쟁점 A(sample16-hwp3 line_segs 보유) diff=0 — 회귀 0 입증. 작업지시자
시각 판정 통과 (sample16-hwp5 page 19~24 겹침 해소, 62→67).

페이지 수 잔존 (HWP5 67 vs HWP3 64)은 후속 #999 시리즈로 분리.
@edwardkim

Copy link
Copy Markdown
Owner

옵션 A로 devel에 반영했습니다 (본질 커밋 de21afa cherry-pick, 작성자 메타데이터 보존, 충돌 없음).

검증:

페이지 수 차이 (HWP5 67 vs HWP3 64 vs HWPX 69)는 PR 본문대로 별도 root cause로 후속 #999 시리즈에서 다룹니다. 잘 분리된 단계적 수정 감사합니다.

@edwardkim edwardkim closed this May 19, 2026
jangster77 added a commit to jangster77/rhwp that referenced this pull request May 19, 2026
Root cause + Fix (2-layer):

A. composer CHARS_PER_LINE 조정 (35 → 45):
   PR edwardkim#997 의 G4 휴리스틱 (35 chars/line) 이 HWP3 reference 평균 (43~46
   chars/line) 보다 작음 → 매 paragraph +1 wrap line → ~2 페이지 inflate.
   HWP3 reference (sample16 pi=443 의 ts=[0,44,87,133]) 측정 결과 기반 조정.

B. typeset spacing_before 보정:
   HWP5 변환본의 line_segs 누락 paragraph 가 ParaShape spacing_before=2264 HU
   (HWP3 reference 1132 HU 의 2x) — Hancom 변환기의 데이터 자체 차이.
   59 paragraph × 1132 HU = ~890 px = ~1 페이지 inflate.
   format_paragraph 에서 line_segs.is_empty() && !text.is_empty() case 에
   spacing_before=0 적용 → HWP3 reference 와 정합.

효과:
- HWP5 sample16-hwp5.hwp: 67 (PR edwardkim#997) → 64 (본 PR) ✓ HWP3 reference 정합
- HWP3 / 다른 HWP5/HWPX sample: 변동 없음
- cargo test --release --lib: 1297 passed, 0 failed
- 240 sample 페이지 수: 타깃 1건 만 (62→64), 회귀 0
- 작업지시자 시각 판정: 통과

영향 범위:
- composer / typeset 내부만 변경 — parser 미변경
- Editor 기능 영향 없음
- HWP5 의 line_segs 누락 case 만 적용 — HWP3 / HWPX (line_segs 있음) 미영향

잔존 (별도 task 예정):
- 자동 보정 (reflow_line_segs) path 의 페이지 수 69 — line_segs 채워서 본
  fix path 우회 → 별도 정합 필요
- HWPX 변종 (72 페이지) — edwardkim#942/edwardkim#988 close 영역 (fundamental 한계)

closes edwardkim#998

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
edwardkim pushed a commit that referenced this pull request May 19, 2026
Root cause + Fix (2-layer):

A. composer CHARS_PER_LINE 조정 (35 → 45):
   PR #997 의 G4 휴리스틱 (35 chars/line) 이 HWP3 reference 평균 (43~46
   chars/line) 보다 작음 → 매 paragraph +1 wrap line → ~2 페이지 inflate.
   HWP3 reference (sample16 pi=443 의 ts=[0,44,87,133]) 측정 결과 기반 조정.

B. typeset spacing_before 보정:
   HWP5 변환본의 line_segs 누락 paragraph 가 ParaShape spacing_before=2264 HU
   (HWP3 reference 1132 HU 의 2x) — Hancom 변환기의 데이터 자체 차이.
   59 paragraph × 1132 HU = ~890 px = ~1 페이지 inflate.
   format_paragraph 에서 line_segs.is_empty() && !text.is_empty() case 에
   spacing_before=0 적용 → HWP3 reference 와 정합.

효과:
- HWP5 sample16-hwp5.hwp: 67 (PR #997) → 64 (본 PR) ✓ HWP3 reference 정합
- HWP3 / 다른 HWP5/HWPX sample: 변동 없음
- cargo test --release --lib: 1297 passed, 0 failed
- 240 sample 페이지 수: 타깃 1건 만 (62→64), 회귀 0
- 작업지시자 시각 판정: 통과

영향 범위:
- composer / typeset 내부만 변경 — parser 미변경
- Editor 기능 영향 없음
- HWP5 의 line_segs 누락 case 만 적용 — HWP3 / HWPX (line_segs 있음) 미영향

잔존 (별도 task 예정):
- 자동 보정 (reflow_line_segs) path 의 페이지 수 69 — line_segs 채워서 본
  fix path 우회 → 별도 정합 필요
- HWPX 변종 (72 페이지) — #942/#988 close 영역 (fundamental 한계)

closes #998

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
edwardkim added a commit that referenced this pull request May 19, 2026
@jangster77 — PR #997 (Task #994 G4) 직접 후속. HWP5 변환본 페이지 수
HWP3 reference 64 vs HWP5 67 해소. 2-layer: composer CHARS_PER_LINE 35→45
(HWP3 line_segs 실측 평균) + typeset spacing_before=0 (line_segs-missing,
HWP3→HWP5 ParaShape 2x 보정).

옵션 A: 본질 커밋 c850148 cherry-pick (작성자 Taesup Jang 보존, 충돌
없음, #997 머지 위 적층). 검증: cargo test 1307 + clippy -D + fmt 0.
sweep 8 fixture (BEFORE devel #997만 ↔ AFTER): 검토 쟁점 A(spacing_before=0
공통 typeset 경로 무차별 — exam/aift/hy-001 일반 샘플 포함) diff=0,
쟁점 B(sample16-hwp3 line_segs 보유) diff=0 — 회귀 0 입증. 작업지시자
시각 판정 통과 (sample16-hwp5 67→64 HWP3 reference 정합, page 19/22/23).

자동보정 path(69) / HWPX 변종(71) / HWP5·HWPX 한컴 권위 페이지 수는
후속 task 분리 (PR 본문 정합).
edwardkim added a commit that referenced this pull request May 19, 2026
@jangster77#997#999 연장. HWP3 변환본 전반 한컴 정합. 격차 A(page
border spec 표136 bit1/2 body_area clip) + B(PUA F03C5→□) + C(변환본
4중 AND 가드 식별 + ParaShape /4) 실효. 격차 D(y_offset double count)
는 후속 커밋 4e3ad58 에서 revert (non-variant table-vpos-01 회귀
발견 후 컨트리뷰터 자정) — 최종 no-op.

옵션 A: 본질 2커밋 cherry-pick (8b7fba3 + 4e3ad58, 작성자 Taesup Jang
보존, orders 충돌 --ours 메인테이너 일지 보존, 소스 .rs 충돌 없음).
검증: cargo test 1307 + clippy -D + fmt 0 + WASM 4.83MB. sweep 8
fixture: 일반 HWP5 6종(exam/aift/biz_plan/복학원서) diff=0 variant=false
유지 (오판 0), HWP3 원본 sample16.hwp 외곽선만 이동·텍스트 무변동.
작업지시자 시각 판정 통과 (sample16-hwp5 page 1/3 + HWP3 원본 외곽선
spec 정합 + 일반 HWP5 무회귀).

잔존 5건 (머릿말 inline image / 페이지 강제나눔 / gradient simplify /
WASM path / 격차 D breathing room) 후속 issue 분리.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants