Skip to content

Release: v0.7.12#970

Merged
edwardkim merged 192 commits into
mainfrom
devel
May 17, 2026
Merged

Release: v0.7.12#970
edwardkim merged 192 commits into
mainfrom
devel

Conversation

@edwardkim
Copy link
Copy Markdown
Owner

v0.7.12 PATCH 릴리즈

v0.7.11 (a9dcdee3) → devel. 외부 기여자 PR 시리즈 흡수 + @jangster77 7-PR 시리즈 (#956~#968) 완결.

핵심 변경

검증

  • cargo test --release ALL GREEN (lib 1288 + integration svg_snapshot 8 + tab_cross_run 1)
  • WASM 재빌드 4.6 MB (rhwp-studio/public 동기화)
  • 422 files / +64761 / -3328

🤖 Generated with Claude Code

dependabot Bot and others added 30 commits May 11, 2026 02:52
Updates the requirements on [skia-safe](https://github.com/rust-skia/rust-skia) to permit the latest version.
- [Release notes](https://github.com/rust-skia/rust-skia/releases)
- [Commits](rust-skia/rust-skia@0.93.1...0.97.0)

---
updated-dependencies:
- dependency-name: skia-safe
  dependency-version: 0.97.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.10 to 8.0.11.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.11/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [workbox-window](https://github.com/googlechrome/workbox) from 7.4.0 to 7.4.1.
- [Release notes](https://github.com/googlechrome/workbox/releases)
- [Commits](GoogleChrome/workbox@v7.4.0...v7.4.1)

---
updated-dependencies:
- dependency-name: workbox-window
  dependency-version: 7.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) from 0.1.40 to 0.1.42.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome)

---
updated-dependencies:
- dependency-name: "@types/chrome"
  dependency-version: 0.1.42
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
…tudio/devel/vite-8.0.11

chore(deps-dev): bump vite from 8.0.10 to 8.0.11 in /rhwp-studio
…tudio/devel/workbox-window-7.4.1

chore(deps-dev): bump workbox-window from 7.4.0 to 7.4.1 in /rhwp-studio
…tudio/devel/types/chrome-0.1.42

chore(deps-dev): bump @types/chrome from 0.1.40 to 0.1.42 in /rhwp-studio
Bumps [puppeteer-core](https://github.com/puppeteer/puppeteer) from 24.42.0 to 24.43.0.
- [Release notes](https://github.com/puppeteer/puppeteer/releases)
- [Changelog](https://github.com/puppeteer/puppeteer/blob/main/CHANGELOG.md)
- [Commits](puppeteer/puppeteer@puppeteer-core-v24.42.0...puppeteer-core-v24.43.0)

---
updated-dependencies:
- dependency-name: puppeteer-core
  dependency-version: 24.43.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
…tudio/devel/puppeteer-core-24.43.0

chore(deps-dev): bump puppeteer-core from 24.42.0 to 24.43.0 in /rhwp-studio
…fe-0.97.0

chore(deps): update skia-safe requirement from 0.93.1 to 0.97.0
Updates the requirements on [resvg](https://github.com/linebender/resvg) to permit the latest version.
- [Release notes](https://github.com/linebender/resvg/releases)
- [Changelog](https://github.com/linebender/resvg/blob/main/CHANGELOG.md)
- [Commits](linebender/resvg@0.45.0...v0.47.0)

---
updated-dependencies:
- dependency-name: resvg
  dependency-version: 0.47.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
chore(deps): update resvg requirement from 0.45 to 0.47
Bumps [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa) from 1.2.0 to 1.3.0.
- [Release notes](https://github.com/vite-pwa/vite-plugin-pwa/releases)
- [Commits](vite-pwa/vite-plugin-pwa@v1.2.0...v1.3.0)

---
updated-dependencies:
- dependency-name: vite-plugin-pwa
  dependency-version: 1.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
…tudio/devel/vite-plugin-pwa-1.3.0

chore(deps-dev): bump vite-plugin-pwa from 1.2.0 to 1.3.0 in /rhwp-studio
macOS Option+Arrow (단어 이동), Option+Shift+Arrow (단어 선택),
Option+Backspace/Delete (단어 삭제) 구현.

- CursorState.moveToWordBoundary() 메서드 추가
- 한글/영문/숫자/공백/구두점 문자 클래스 기반 경계 탐지
- 본문 및 표 셀 내부 모두 지원
- Alt+Arrow가 기존 Alt 단축키 핸들러에 잡히지 않도록 가드 추가
1. Alt+Backspace/Delete 주석: 'Backspace' → 'Backspace/Delete'
2. moveToWordBoundaryInCell: getTextInCell(0, 9999) 전체 fetch 대신
   getCellParagraphLength로 길이 확인 후 필요 구간만 슬라이스
작업지시자 시각 검증 영역 발견: 'Alt+Delete 만 동작하지 않습니다.'

본질: shortcut-map.ts:97 영역 영역 Alt+Delete → table:delete-col 매핑 (5/10 이전 등록) 영역 영역 일반 편집 영역 영역 dispatcher.dispatch 영역 영역 silently fail (canExecute=inTable 차단) + return → switch (e.key) case 'Delete' 영역 영역 도달 부재 → 단어 삭제 미동작.

정정: Alt 조합 단축키 가드 영역 영역 Alt+Delete 영역 영역 표 안/외 분기 추가:
- 표 안 + Alt+Delete: table:delete-col (칸 지우기, 기존 동작 보존)
- 표 외 + Alt+Delete: 다음 단어 삭제 (PR #794 본질)
- Alt+Backspace: 이전 단어 삭제 (항상)
- Alt+Arrow: 단어 이동 (항상)

PR #740 자기 정정 패턴 정합.
PR #746 (Ctrl/Cmd+Arrow 줄/문서 시작·끝) 후속 영역 영역 Issue #223 의 다음 단계.

4 단축키:
- Option+←/→: 단어 단위 커서 이동
- Option+Shift+←/→: 단어 단위 선택 확장
- Option+Backspace: 이전 단어 삭제
- Option+Delete: 다음 단어 삭제

신규 인프라 — CursorState.moveToWordBoundary:
- 본문 (getTextRange) + 표 셀 (getTextInCell) 양 경로 지원
- 문자 클래스 5종 (공백/한글/Latin/Digit/Punct)
- 슬라이스 50 char cap (성능 가드)

본 환경 자기 정정 commit (7c0418f) — Alt+Delete 영역 영역 충돌 정정:
- shortcut-map.ts:97 영역 영역 Alt+Delete → table:delete-col 매핑 (이전 등록) 영역 영역 일반 편집 영역 영역 dispatcher silently fail + return → 단어 삭제 미동작.
- 정정: 표 안/외 분기 (옵션 1 작업지시자 결정):
  - 표 안 + Alt+Delete: table:delete-col (칸 지우기, 기존 동작 보존)
  - 표 외 + Alt+Delete: 다음 단어 삭제 (PR #794 본질)
  - Alt+Backspace / Alt+Arrow: 항상 단어 삭제/이동

검증:
- tsc --noEmit ✅
- cargo test --release ALL GREEN
- 광범위 sweep 170/170 same
- WASM 4.68 MB 재빌드
- 작업지시자 시각 검증 ✅ 통과 (자기 정정 후)

Part of #223
PR #794 (Part of #223) — Alt/Option+Arrow 단어 단위 커서 이동 + 자기 정정 commit (Alt+Delete 표 안/외 분기).
작업지시자 결정 옵션 1 — 표 안 = 칸 지우기 (기존) / 표 외 = 단어 삭제 (PR #794).

Merge commit: 76e242c
Cherry-pick: 6b2fcd7 + a32895b
자기 정정: 7c0418f
드래그 중 hitTest 결과가 anchor와 다른 셀(또는 본문)을 가리키면
커서 이동을 무시하여 셀 내부 선택이 유지되도록 수정.

원인: 셀 내부의 빈 영역(텍스트 라인 아래)을 드래그하면
hitTest가 본문 레벨 위치를 반환하여 셀↔본문 혼합 선택이
발생하고, updateSelection에서 렌더링이 생략되는 문제.
표 셀 내부 텍스트 드래그 선택 시 선택 하이라이트 미렌더링 결함 정정 (Issue #669).

본질: 드래그 중 hitTest 가 셀 내부 빈 영역에서 본문 레벨 위치 반환 → anchor(셀) ↔ focus(본문) 혼합 → updateSelection() 영역 영역 selectionRenderer.clear() 호출 → 선택 미렌더링.

정정 (input-handler.ts:1014 updateTextSelectionDragFromPointer 래퍼 안 +14):
- anchor 셀 내부일 때 hit 가 같은 셀 컨텍스트 (parentParaIndex/controlIndex/cellIndex 모두 일치) 가 아니면 cursor.moveTo 건너뜀
- 셀 내 선택 유지

본 환경 충돌 수동 해결: HEAD (devel) 영역 영역 PR #718 (Task #661) updateTextSelectionDragFromPointer 래퍼 사용 영역 영역 incoming 의 onMouseMove 영역 영역 직접 hit + moveTo 영역 영역 비대칭. 본 환경 영역 영역 셀 가드 영역 영역 input-handler.ts 의 래퍼 안 적용 (PR #718 정합성 보존).

검증:
- tsc --noEmit ✅
- cargo test --release ALL GREEN
- 광범위 sweep 170/170 same
- WASM 4.68 MB 재빌드
- 작업지시자 웹 에디터 시각 검증 ✅ 통과

closes #669
PR #795 (closes #669) — 표 셀 내부 드래그 선택 시 셀 컨텍스트 이탈 방지.
충돌 수동 해결: 셀 가드 영역 input-handler.ts 의 updateTextSelectionDragFromPointer 래퍼 안 적용 (PR #718 정합성 보존).

Merge commit: 58176ed
Cherry-pick: 2efe20c
…ract (P11)

P11 단계 — P9 영역 영역 text replay parity 후속 영역 영역 Text IR v2 compatibility contract 추가.

Skia native raster Issue #536 트래킹 단계적 진전:
P4 #599 → P5 #626 → P6 #720 → P8 #761 → P9 #769 → P11 #797

중요: 본 PR 은 GlyphRun 을 기본 경로로 만드는 PR 이 아니라 TextRun v2 compatibility contract 를 완성하는 PR.

4 본질 원칙:
- Compatibility first — 모든 backend 가 TextRun fallback 으로 렌더링 가능
- Additive schema — schemaMinorVersion + feature negotiation (기존 consumer 미파괴)
- Source traceability — text_sources + TextRun.source span
- Placement/cluster metadata — paintStyle/projectionKind/orientation/placement/clusterBasis/clusters/legacyVisuals

신규 인프라:
- PaintOp::{CharOverlap, TextControlMark, TabLeader, TextDecoration} — explicit visual ops
- PageLayerTree.text_sources + TextSourceTable (export-local)
- TextRun.source span + 7 신규 metadata
- schemaMinorVersion + resourceTableMinorVersion + feature negotiation
- docs/text-ir-v2.md migration contract

Renderer 정정 — 4 backend 동기 (feedback_image_renderer_paths_separate 권위 사례):
- svg_layer.rs / canvas.rs / skia/renderer.rs / web_canvas.rs — 신규 special visual op skip (double-painting 방지)

Non-goals (Still designing):
- GlyphRun eligibility / font resource table / cluster basis / fallback diagnostics — P12+ 분리

검증:
- cargo build/test/clippy --release ALL GREEN (clippy -D warnings)
- native-skia 28/28 PASS (PR #769 인프라 보존)
- 광범위 sweep 7 fixture / 170 페이지 / 회귀 0 (Compatibility first 원칙 입증)
- 시각 판정 면제 (작업지시자 결정 — contract 정합 단계 + 결정적 검증 + sweep 통과)

Refs #536
PR #797 (Refs #536) — P11 Text IR v2 compatibility contract.
Skia native raster 트래킹 단계적 진전 (P4→P5→P6→P8→P9→P11).

Merge commit: 098db01
Cherry-pick: 795a132
- Global anchors, relative isNearStructure base, norm-hash pins
- rightToLeftPara from alignment into buildControlDiffs slot matching

Co-authored-by: Cursor <cursoragent@cursor.com>
… 반영)

- upstream devel 기준에서 삭제됐던 compare/history UI 파일 복원 + main 기준 session/compare-debug/history 모듈 추가
- diff-engine stash 변경과 WasmBridge 비교용 API(hasLoadedDocument, refreshLayout, getDocumentInfo, stable_id, 표/도형 서명) 정합
- PageInfo.pageNumber 선택 필드 추가

Co-authored-by: Cursor <cursoragent@cursor.com>
- edit:compare-documents / edit:document-history 등록 (CompareSessionStore 공유)
- index.html 편집 메뉴·찾기 스플릿·툴바 버튼 반영
- shortcut-map: Alt+Shift+V, Ctrl+Shift+H

Co-authored-by: Cursor <cursoragent@cursor.com>
PR #799 의 5번째 commit 에 잘못 staging 된 *.actual.svg 5 파일 제거:
- tests/golden_svg/form-002/page-0.actual.svg
- tests/golden_svg/issue-147/aift-page3.actual.svg
- tests/golden_svg/issue-157/page-1.actual.svg
- tests/golden_svg/issue-267/ktx-toc-page.actual.svg
- tests/golden_svg/table-text/page-0.actual.svg

*.actual.svg 는 테스트 실행 시 생성되는 임시 출력 파일로,
golden (*.svg) 와 비교만을 위한 산출물이다. 저장소에 포함되지 않아야 한다.
edward.kim and others added 24 commits May 17, 2026 18:06
## 본질

`4bb11289 fix: 쪽테두리 종이기준/본문기준 bit 해석 반전 정정 (closes #920)` 의
`paper_based = (attr & 0x01) == 0` 비트 해석이 hwp3-sample16 / 시험지 등 다수 sample 에서 회귀.

## 분석

5+ samples (sample16, 시험지, biz_plan, 국립국어원, text-align-2, pua-test) 의 한컴 viewer 실측:
- attr bit 0 = 0/1 양쪽 다 paper-based outline 렌더
- HWPX textBorder=PAPER/CONTENT 양쪽 다 paper-based
- fillArea="PAPER" 공통

→ bit 0 은 outline 위치 결정 비트 아님 (text wrap interaction 등 다른 의미).

## Fix

`paper_based = true` 강제 (한컴 viewer 실측 정합). 회귀 history 코멘트 기록.
`RHWP_DEBUG_PAGE_BORDER` 환경변수 영구화 (attr 추적 진단 도구).

## 잔존 회귀 (별도 task 분리)

- Issue 2 — sample16 page 18 본문 다음 페이지 밀림 (장기 typeset 결함, 회귀 commit 없음)
- Issue 3 — 시험지 page 1 문9 vertical 처짐 (HWP5 column layout)

본 task #952 범위는 Issue 1 (외곽선) 만 해결.

## 검증

- cargo test --release --lib: 1288 passed, 0 failed
- sample16 HWP3/HWP5/HWPX page 17 외곽선: paper-based (x=18.93~774.77) ✓ task877 baseline 정합
- 시험지 (3-11월) HWP5/HWPX page 1 외곽선: paper-based (x=26.45~767.25) ✓ 한컴 정합

## 추가 fixture (samples + pdf)

- samples/3-09월_교육_통합_2022.{hwp,hwpx}, 3-09월_교육_통합_2023.{hwp,hwpx}
- samples/3-10월_교육_통합_2022.{hwp,hwpx}, 3-11월_실전_통합_2022.{hwp,hwpx}
- pdf/3-09월_교육_통합_2022.pdf 외 3개 — 한컴 2022 권위 PDF

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jangster77 — Issue #952 Issue 1: 페이지 외곽선 paper/body 잘못 분류 회귀.

회귀 source: 4bb1128 fix (#920) 의 paper_based = (attr & 0x01) == 0 비트 반전.
회귀 history: task877 (!= 0, sample16 정합/시험지 회귀) → #920 (== 0, 시험지 정합/sample16 회귀)
→ 본 PR (true, 모든 sample 한컴 정합).

진단: 5+ samples 한컴 viewer 실측 — attr 0/1, textBorder PAPER/CONTENT 양쪽 다
paper-based. bit 0 은 outline 위치 결정 비트 아님 (text wrap interaction 등 별 의미).

본질 (src/renderer/layout.rs +16/-2):
- paper_based = true 강제 (#920 회귀 코드 대체)
- RHWP_DEBUG_PAGE_BORDER 환경변수 진단 영구화
- 회귀 history 코멘트 영구 보존 (재회귀 방지)

추가: 시험지 fixture 8 (HWP/HWPX) + 한컴 2022 권위 PDF 4 (회귀 가드) + 문서 4.

본 PR 범위 외 (별도 task 분리, PR 본문 명시):
- Issue 2: sample16 page 18 본문 밀림 (typeset multi-TAC cursor over-advance)
- Issue 3: 시험지 문9 vertical 처짐 (HWP5 column)

자기 검증: cargo test --release --lib 1288 passed / clippy 통과 /
광범위 sweep 7 fixture / 169 페이지 / 169 same / 0 diff / WASM 4.4 MB 재빌드
시각 판정: 작업지시자 시각 검증 통과 (sample16 + 시험지 page border paper-based,
한컴 2022 PDF 권위)
CI: ✅ Build & Test + CodeQL + Canvas visual diff

연속 5 PR 1번째 (#956#958#961#963#964, @jangster77 순차 처리)
- mydocs/pr/archives/pr_956_review.md (paper-based outline 회귀 분석)
- mydocs/pr/archives/pr_956_report.md (옵션 A 처리 결과 + 동기화 선행)
- mydocs/orders/20260517.md PR 처리 섹션 추가 (컨트리뷰터 Task #952 작업 일지 보존)

핵심:
- 사전 동기화 (5/11→5/17 +19 commits FF, feedback_release_sync_check)
- 회귀 source 4bb1128 (#920) bit 반전 → paper_based=true 강제
- 5+ samples 한컴 실측 정합 (추측보다 실측)
- sweep 169/169 same + 작업지시자 시각 판정 통과
- Issue #952 OPEN 유지 (Issue 2/3 별도 task, 연속 PR #958 후속)
…loses #957)

`samples/hwp3-sample16.hwp` page 18 의 "나. 주요 과업내용" 후 본문 paragraphs (pi=395~401 "○ 통합모델...") 이 다음 페이지로 밀려 시각 누락. 한컴 viewer 는 같은 페이지 표시.

`RHWP_DEBUG_TAC_CURSOR` 추적 결과:

```
Shape pi=394 ci=1 y_in=767.3 y_out=1197.9 dy=430.6 ⚠️
FullPara pi=395 y_in=1197.9 (body 외 영역)
```

`src/renderer/layout.rs:3470-3475` (layout_shape_item 의 Bottom caption advance):
- pi=394 ci=1 picture 의 caption: `dir=Bottom width=0 paras=1 text=""` (빈 caption)
- 빈 caption 임에도 layout 이 `cap_bottom = cap_y + caption_h` 계산
- pic_y = 767.3 (has_prior_tac 로 갱신된 잘못된 para_start_y)
- image_bottom = 767.3 + 411.89 (pic_h) = 1179.19
- cap_y = 1179.19, caption_h = 18.7 (empty paragraph default line height)
- cap_bottom = 1197.89 → result_y +430.6 phantom advance
- 다음 paragraph (pi=395) 가 1197.89 부터 시작 → body 외 emit

빈 caption 시 result_y advance skip — `paragraphs[*].text` 가 모두 무의미 문자 + `controls.is_empty()` 시 advance 분기 미진입.

`RHWP_DEBUG_TAC_CURSOR` 환경변수 영구화 (paragraph item 별 y_offset 추적 진단 도구).

- cargo test --release --lib: 1288 passed, 0 failed, 2 ignored
- sample16 page 18: pi=395~401 본문 같은 페이지 정상 emit (한컴 viewer page 16 정합)
- hwp3-sample14 (Task #864 영역, non-empty caption "Cut&Paste 할 영역" + empty 다수): 정상
- hwp3-sample10/11/13, exam_kor/math: 회귀 없음

| 영역 | 영향 |
|------|------|
| Empty caption picture | result_y advance 제거 (회귀 fix) |
| Non-empty caption picture | 영향 없음 |
| Caption None | 영향 없음 |
| Caption Top direction | 영향 없음 |

- 원 issue #952 + PR #956 — Issue 1 (페이지 외곽선 paper/body) 해결
- archive/task936 — 본 영역 이전 fix 시도 history (미완)
- PR #918 — 본 영역 시도 history (close)
- 잔존 — 원 #952 의 Issue 3 (HWP5 시험지 page 1 문9 vertical) 별도 task

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jangster77 — Issue #957 (Issue #952 영역 영역 Issue 2 분리 task): sample16 page 18
"나. 주요 과업내용" 후 본문 (pi=395~401) 다음 페이지 밀림.

Root cause (RHWP_DEBUG_TAC_CURSOR 추적): pi=394 ci=1 picture 의 빈 caption
(dir=Bottom width=0 paras=1 text="") 영역 영역 phantom +430.6px 누적 →
pi=395 가 body 외 영역 emit → 다음 페이지 밀림.

본질 (src/renderer/layout.rs): caption_is_empty 가드 — caption 모든 paragraph
영역 영역 무의미 문자 (≤U+001F, U+FFFC) + controls.is_empty() 시 result_y
advance skip. RHWP_DEBUG_TAC_CURSOR 진단 영구화 (PR #956 RHWP_DEBUG_PAGE_BORDER
패턴 정합).

영역 좁힘: 빈 caption + Bottom 한정 — non-empty/None/Top 영향 없음.

본 환경 충돌 수동 해결:
- mydocs/orders/20260517.md — 본 환경 PR #956 처리 섹션 + PR #958 Task #957
  작업 일지 양측 보존 통합
- src/renderer/layout.rs — PR #956 (paper_based :770) + PR #958 (caption :3491)
  다른 영역 auto-merge (양립 확인)

자기 검증: cargo test --release --lib 1288 passed / clippy 통과 /
광범위 sweep 7 fixture / 169 페이지 / 169 same / 0 diff / WASM 4.4 MB 재빌드
시각 판정: 작업지시자 시각 검증 통과 (sample16 page 18 본문 같은 페이지,
sample14/10/11/13 + PR #956 page border 회귀 부재)
CI: ✅ Build & Test + CodeQL + Canvas visual diff

연속 5 PR 2번째 (#956 ✅ → #958#961#963#964)
- mydocs/pr/archives/pr_958_review.md (빈 caption phantom advance 분석)
- mydocs/pr/archives/pr_958_report.md (옵션 A 처리 결과)
- mydocs/orders/20260517.md PR #958 행 추가

핵심:
- pi=394 ci=1 picture 빈 caption phantom +430.6px (RHWP_DEBUG_TAC_CURSOR 추적)
- caption_is_empty 가드 + Bottom 한정 (케이스별 명시)
- PR #956 + #958 layout.rs 양립 확인 (paper_based :770 + caption :3491)
- sweep 169/169 same + 작업지시자 시각 판정 통과
- Issue #957 close, Issue #952 OPEN 유지 (Issue 3 잔존, 연속 PR #961 후속)
…p — 시험지 page 1 문9 정합 (closes #959)

`samples/3-11월_실전_통합_2022.hwp` page 1 우측 단의 문9 가 한컴 viewer 보다 ~250px 아래 처짐.

`RHWP_DEBUG_TAC_CURSOR` 추적 결과:

```
Shape pi=69 ci=0 y_in=709.4 y_out=983.4 dy=274.0 ⚠️
... pi=70~72 (빈 줄) ...
FullPara pi=73 y_in=1043.5 ... (문9 line)
```

pi=69 picture (16786×20400 HU = 59.2×72.0mm, **height 272px**):
- horz_rel_to=Column, h_offset=79.5mm (300px), 정렬=Center
- vert_rel_to=Para, wrap=TopAndBottom, tac=false

`compute_object_position` 계산:
- pic_emit_x = col_area.x + (col_width - pic_width)/2 + h_offset = 399 + (360-224)/2 + 300 = 767
- col_area right = 759.7 → **picture 가 column 우측 외부 emit**

한컴 viewer 는 column flow 에 reservation 하지 않음 (PDF 분석: 우측 단에 picture 표시 안 됨). rhwp 는 picture height 만큼 cursor advance → 문9 처짐.

`src/renderer/layout.rs:3500-3556`: horz_rel_to=Column picture 의 emit x 사전 계산.
pic_emit_x >= col_area.x + col_area.width 시 result_y = saved_y_offset (advance skip).

`RHWP_DEBUG_TAC_CURSOR` 환경변수 영구화 (paragraph item 별 y_offset 추적 진단 도구).

- cargo test --release --lib: 1288 passed, 0 failed
- 시험지 (3-11월) page 1 문9: y=1061 → y=805 ✓ 한컴 PDF 정합
- pi=69 ci=0 dy: 274 → 18 (line advance만) ✓
- 시험지 4종 (3-09월 2022/2023, 3-10월 2022, 3-11월 2022) page 1: 정상
- exam_kor page 18 (Square wrap picture, Task #722 영역): 정상
- exam_math/eng/hwp3-sample10/11/13: 정상

| 영역 | 영향 |
|------|------|
| horz_rel_to=Column + col 외부 emit picture | result_y advance 제거 (회귀 fix) |
| horz_rel_to=Column + col 내부 emit picture | 영향 없음 (Fix 조건 미진입) |
| horz_rel_to=Paper/Page picture | 영향 없음 (is_paper_based 분기) |
| horz_rel_to=Para picture | 영향 없음 (Column 검사 false) |
| TAC picture | 영향 없음 (별도 분기) |

본 task page 2 시각 검증 중 multi-line equation off-by-one 결함 발견 → 별도 issue #960 (Fix C 와 무관 pre-existing).

- 원 issue #952 + PR #956 — Issue 1 (페이지 외곽선) 해결
- PR #958 — Issue 2 (sample16 page 18 본문 누락) 해결
- 본 PR #959 — Issue 3 (시험지 page 1 문9 vertical) 해결
- 신규 issue #960 — page 2 multi-line equation off-by-one (별도)

PR #956 와 중복 가능 (먼저 머지되는 PR 가 가져감):
- samples/3-09월_교육_통합_2022.{hwp,hwpx} 외 7개
- pdf/3-09월_교육_통합_2022.pdf 외 3개

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… column 외부 emit advance skip

@jangster77 — Issue #959 (Issue #952 영역 영역 Issue 3 분리 task): 시험지 (3-11월)
page 1 우측 단 문9 ~250px 처짐.

Root cause (RHWP_DEBUG_TAC_CURSOR 추적): Shape pi=69 ci=0 picture
(horz_rel_to=Column, h_offset=79.5mm, Center) 영역 영역 pic_emit_x=767 >
col_area right=759.7 → picture 좌측 edge column 외부, 한컴 PDF 우측 단 미표시.
그럼에도 cursor 274px advance → 문9 처짐.

본질 (src/renderer/layout.rs:3537): saved_y_offset — horz_rel_to=Column
picture pic_emit_x 가 col_area 우측 초과 시 result_y = saved_y_offset
(advance skip, :3566).

영역 좁힘: horz_rel_to=Column + col 외부 한정 — col 내부/Paper/Page/Para/TAC
영향 없음.

본 환경 충돌 수동 해결:
- mydocs/orders/20260517.md — 본 환경 PR #956/#958 처리 표 + PR #961
  Task #959/#960 작업 일지 양측 보존 통합
- src/renderer/layout.rs — auto-merge: PR #956 paper_based=true :770 +
  PR #958 caption_is_empty :3491 + PR #961 saved_y_offset :3537 3 정정 양립
- 시험지 fixture 8 + PDF 4 — PR #956 머지 영역 영역 동일 content (devel 보존)

자기 검증: cargo test --release --lib 1288 passed / clippy 통과 /
광범위 sweep 7 fixture / 169 페이지 / 169 same / 0 diff / WASM 4.4 MB 재빌드
시각 판정: 작업지시자 시각 검증 통과 (시험지 page 1 문9 y=805 한컴 PDF 정합,
시험지 4종 + exam_kor p18 + PR #956/#958 회귀 부재)
CI: ✅ Build & Test + CodeQL + Canvas visual diff

Issue #952 종결 — Issue 1 (#956) + Issue 2 (#958) + Issue 3 (#961) 모두 해결.
잔존: Issue #960 (page 2 multi-line equation off-by-one, pre-existing, 별도)
연속 5 PR 3번째 (#956 ✅ → #958 ✅ → #961#963#964)
- mydocs/pr/archives/pr_961_review.md (column picture advance skip 분석)
- mydocs/pr/archives/pr_961_report.md (옵션 A 처리 결과 + Issue #952 종결)

핵심:
- pi=69 ci=0 horz_rel_to=Column picture col 외부 emit (RHWP_DEBUG_TAC_CURSOR)
- saved_y_offset advance skip + col 외부 한정 (케이스별 명시)
- PR #956/#958/#961 layout.rs 3 정정 양립 (:770 + :3491 + :3537)
- sweep 169/169 same + 작업지시자 시각 판정 통과
- Issue #952 종결 (Issue 1/2/3 모두 해결: #956/#958/#961)
- 잔존 Issue #960 (page 2 equation off-by-one, 별도 task)
… formula off-by-one 해소 (closes #960)

`samples/3-11월_실전_통합_2022.hwp` page 2 문14 (pi=117) 의 cases formula (multi-line equation g(x)={cases x...f(x)}) 가 line 0 영역 (y=329) 에 emit (예상 line 1 ~y=347, 한컴 PDF 정합) — header text 와 시각 overlap.

`RHWP_DEBUG_PARA_TAC` 추적 결과:

pi=117 text 에 FFFC (object replacement char) 없음. `find_control_text_positions` (model/paragraph.rs:817-838) 의 char_offsets gap 분석:
- utf16 gap [60, 76] = 15 → 1 control at codepoint position 30 (= `\n`)

compose_lines 의 line 1 chars range = [23, 30) — **position 30 (=\n) 제외**.

`paragraph_layout.rs:1724-1727` filter:
```rust
.filter(|(pos, _, _)| *pos >= run_char_pos
    && (*pos < run_char_end
        || (is_last_run && *pos == run_char_end)))
```

cases (pos=30) for line 1 (run_char_pos=23, run_char_end=30):
- pos < 30 ❌, is_last_run ❌ (line 1 은 paragraph 의 last line 아님)
- → cases 가 line 1 run_tacs 에서 누락 → shape_layout 의 default y (=329) 에 emit

`src/renderer/layout/paragraph_layout.rs:1719-1736`:

```rust
// [Task #960] has_line_break line 의 마지막 run 도 run_char_end 위치 의 TAC 포함.
// HWP3 의 char_offsets gap 분석으로 매핑된 control 위치가 `\n` 문자에 떨어지면,
// 그 line 의 chars range [start, end) 에서 end 가 `\n` 위치이므로 누락.
let allow_end_tac = is_last_run
    || (comp_line.has_line_break && is_last_run_of_line(run_idx));
let run_tacs: Vec<(usize, f64, usize)> = tac_offsets_px.iter()
    .filter(|(pos, _, _)| *pos >= run_char_pos
        && (*pos < run_char_end
            || (allow_end_tac && *pos == run_char_end)))
    ...
```

- cargo test --release --lib: 1288 passed, 0 failed
- 시험지 page 2 문14 cases formula y: 329 → 352 ✓ (line 1 정상)
- TAC_LINE pi=117 line 1 run_tacs: [] → [(7, 177.85, 3)] ✓
- LAYOUT_OVERFLOW count: 41 → 41 (회귀 0)
- exam_kor/math/eng, sample10~14, 시험지 4종: 시각 회귀 0

| 영역 | 영향 |
|------|------|
| has_line_break + end-position control | 정상화 (이전 누락 → line 내 inline emit) |
| has_line_break 없는 line | 영향 없음 (조건 미진입) |
| 일반 TAC control (line 안쪽) | 영향 없음 |

본 task Stage 4 시각 검증 중 발견:
- 문14 의 <보기> textbox (pi=118 InFrontOfText TAC 사각형 + 내부 글상자) 의 inline 수식 + ㄱㄴㄷ prefix scramble
- Fix 적용 전/후 동일 (pre-existing, Fix 와 무관)
- → 별도 issue #962 등록

- 원 issue #952 + PR #956 (Issue 1 외곽선)
- PR #958 (Issue 2 sample16 page 18)
- PR #961 (Issue 3 시험지 page 1 문9 vertical)
- 본 PR (Issue 4 cases formula off-by-one)
- 신규 issue #962 — page 2 보기 textbox 별도 task

samples/pdfs (시험지 hwp/hwpx, pdf) 는 PR #956 에서 추가되므로 본 PR 에 포함 안 함.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jangster77 — Issue #960 (Issue #952 영역 영역 Issue 4, PR #961 page 2 검증 중 발견):
시험지 (3-11월) page 2 문14 (pi=117) cases formula 가 line 0 (y=329) 에 emit
→ header text 와 overlap.

Root cause (RHWP_DEBUG_PARA_TAC 추적): char_offsets gap 분석 영역 영역 cases (ci=3)
→ position 30 (= \n 위치). compose_lines line 1 chars [23,30) 에서 position 30
제외 → is_last_run && pos == end 만 허용 → line 1 (last line 아님) 누락 →
line 0 (header) emit → overlap.

본질 (src/renderer/layout/paragraph_layout.rs:1730):
allow_end_tac = is_last_run || (comp_line.has_line_break && is_last_run_of_line(run_idx))
— has_line_break line 의 마지막 run 도 end position TAC 포함.
RHWP_DEBUG_PARA_TAC 진단 영구화.

영역 좁힘: has_line_break + end-position 한정 — has_line_break 없는 line /
일반 TAC (line 안쪽) 영향 없음.

본 환경 충돌 수동 해결: mydocs/orders/20260517.md (--ours 본 환경 PR 처리 표
보존 + Task #960/#962 작업 일지 갱신). paragraph_layout.rs auto-merge
(devel 변경 부재, PR #956/#958/#961 은 layout.rs — 다른 파일, 4 정정 양립).

자기 검증: cargo test --release --lib 1288 passed / clippy 통과 /
광범위 sweep 7 fixture / 169 페이지 / 168 same / 1 diff (exam_math_017.svg).
exam_math page 18 inline equation line 매핑 변화 — 작업지시자 한컴 2020 에디터
직접 확인 영역 영역 **의도된 정정 확정** (이전 잘못된 line emit → 정정 후 올바른
line, 시험지 page 2 cases 와 동일 본질). WASM 4.4 MB 재빌드.

시각 판정: 작업지시자 시각 검증 통과 (시험지 page 2 cases y=352 한컴 PDF 정합
+ exam_math page 18 의도 확정 + 회귀 부재)
CI: Build & Test pending (devel merge commit 재트리거) — 본 환경 자기 검증 보완

잔존: Issue #962 (page 2 보기 textbox scramble, pre-existing, 별도 task)
연속 5 PR 4번째 (#956 ✅ → #958 ✅ → #961 ✅ → #963#964)
- mydocs/pr/archives/pr_963_review.md (TAC line 매핑 off-by-one 분석)
- mydocs/pr/archives/pr_963_report.md (옵션 A + exam_math p18 sweep diff 의도 확정)
- mydocs/orders/20260517.md PR #963 행 추가

핵심:
- pi=117 cases formula position 30 (\n) off-by-one (RHWP_DEBUG_PARA_TAC)
- allow_end_tac (has_line_break line 마지막 run end-position TAC 포함)
- PR #956/#958/#961 (layout.rs) + #963 (paragraph_layout.rs) 4 정정 양립
- sweep 168/169 same + exam_math p18 diff 1건 → 작업지시자 한컴 2020 직접 확인 의도 확정
- Issue #960 close, Issue #962 잔존 (별도 task, 연속 PR #964 후속)
…ox content scramble 해소 (closes #962)

`samples/3-11월_실전_통합_2022.hwp` page 2 문14 <보기> textbox (pi=118 InFrontOfText TAC 사각형 + 내부 글상자) 의 inline 수식이 각각 2번 emit → ㄱㄴㄷ prefix + 본문 + 수식 시각 overlap.

`RHWP_DEBUG_PARA_TAC` + SVG 분석:
- 보기 textbox 영역 (y 440-540, x 400-760) 의 equation transforms: **12 개** (예상 6 × 2 duplicates)
- Set 1 (gap 위치, 정상): paragraph_layout 의 inline TAC 처리 (paragraph_layout.rs:2078+)
- Set 2 (textbox 좌측 edge x=406): shape_layout 두번째 loop 의 Equation branch (shape_layout.rs:1609)

원래 두번째 loop 의 의도: paragraph_layout 미지원 이전의 legacy fallback. 현재 paragraph_layout 가 textbox 내부 inline TAC 를 정상 처리하므로 중복 emit 발생.

`src/renderer/layout/shape_layout.rs:1609-1675`:

```rust
Control::Equation(eq) => {
    let eq_w = hwpunit_to_px(eq.common.width as i32, self.dpi);
    let eq_h = hwpunit_to_px(eq.common.height as i32, self.dpi);
    // [Task #962] 글상자 내부 paragraph 의 inline equation 은 paragraph_layout 가
    // 정확한 gap 위치 (text 사이) 에 emit. 본 두번째 loop 는 legacy fallback.
    let equiv_cell_ctx = CellContext {
        parent_para_index: para_index,
        path: { /* parent_cell_path + textbox entry */ },
    };
    if tree.get_inline_shape_position(
        section_index, pi, ctrl_idx_in_para, Some(&equiv_cell_ctx)
    ).is_some() {
        // paragraph_layout 가 이미 emit — inline_x 만 advance
        inline_x += eq_w;
    } else {
        // legacy fallback (기존 emit 분기 유지)
        ...
    }
}
```

- cargo test --release --lib: 1288 passed, 0 failed
- 시험지 page 2 보기 textbox equations: 12 → 6 ✓ (duplicates 제거)
- 시각: ㄱ. h(1)=3 / ㄴ. 함수 h(x)는... / ㄷ. 함수 g(x)가... ✓ 한컴 PDF 정합
- LAYOUT_OVERFLOW count: 325 → 325 (회귀 0)
- exam_kor/math/eng, sample14, 시험지 4종: 시각 회귀 0

| 영역 | 영향 |
|------|------|
| textbox 내부 inline Equation (paragraph_layout 등록 case) | duplicate 제거 (회귀 fix) |
| textbox 내부 inline Equation (legacy fallback) | 영향 없음 (else 분기 유지) |
| textbox 내부 Shape/Picture/Table | 본 fix 미대상 |
| textbox 외부 standalone equation | 영향 없음 |

- 원 issue #952 + PR #956 (Issue 1 외곽선)
- PR #958 (Issue 2 sample16 page 18)
- PR #961 (Issue 3 시험지 page 1 문9 vertical)
- PR #963 (Issue 4 cases formula off-by-one)
- 본 PR (Issue 5 보기 textbox duplicate equation)

원 #952 의 5 issue 모두 해결 → close 가능.

samples/pdfs (시험지) 는 이전 PR (#956/#961) 에서 추가되므로 본 PR 에 포함 안 함 (중복 방지).
layout.rs 변경은 PR #958/#961 영역 — 본 PR 에 포함 안 함.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jangster77 — Issue #962 (Issue #952 영역 영역 Issue 5, PR #963 page 2 검증 중 발견):
시험지 (3-11월) page 2 문14 <보기> textbox inline 수식 각각 2번 emit → scramble.

Root cause (SVG 분석): 보기 textbox 영역 영역 equation 12개 (6×2 duplicates):
- Set 1 (정상, gap 위치): paragraph_layout inline TAC (paragraph_layout.rs:2078+)
- Set 2 (duplicate, textbox 좌측 edge): shape_layout 두번째 loop legacy fallback
paragraph_layout 가 현재 textbox 내부 inline TAC 정상 처리 → legacy fallback 중복.

본질 (src/renderer/layout/shape_layout.rs:1620): Equation branch emit 전
get_inline_shape_position (equiv_cell_ctx — parent_cell_path + textbox entry)
확인 → paragraph_layout 등록 시 inline_x += eq_w 만 (duplicate 차단),
미등록 시 legacy fallback 유지.

영역 좁힘: paragraph_layout 등록 case 한정 — legacy fallback /
Shape/Picture/Table / standalone 영향 없음.

본 환경 충돌 수동 해결: mydocs/orders/20260517.md (--ours 본 환경 PR 처리 표
보존 + Task #962 작업 일지 갱신). shape_layout.rs auto-merge (devel 변경 부재).
PR #956/#958/#961 (layout.rs) + #963 (paragraph_layout.rs) + 본 PR
(shape_layout.rs) — **5 정정 모두 다른 파일, 양립 확인**.
devel merge commit (93dfe0a/3d1cdf31) cherry-pick 제외 — 본질 682875f 만.

자기 검증: cargo test --release --lib 1288 passed / clippy 통과 /
광범위 sweep 7 fixture / 169 페이지 / 169 same / 0 diff / WASM 4.4 MB 재빌드
시각 판정: 작업지시자 시각 검증 통과 (시험지 page 2 보기 textbox 12→6,
ㄱ/ㄴ/ㄷ 한컴 PDF 정합 + 회귀 부재)
CI: ✅ Build & Test + CodeQL + Canvas visual diff

원 Issue #952 의 5 분리 결함 완결:
#952 Issue 1 (#956) + Issue 2 (#958) + Issue 3 (#961) + #960 (#963) + #962 (본 PR)
연속 5 PR 완결 (@jangster77: #956#958#961#963#964)
- mydocs/pr/archives/pr_964_review.md (textbox inline equation duplicate 분석)
- mydocs/pr/archives/pr_964_report.md (옵션 A + Issue #952 5 분리 결함 완결)
- mydocs/orders/20260517.md PR #964 행 + 연속 5 PR 총평

핵심:
- 보기 textbox equation 12개 (Set 1 paragraph_layout + Set 2 shape_layout legacy duplicate)
- get_inline_shape_position (equiv_cell_ctx) 확인 영역 영역 duplicate 차단
- PR #956~#964 5 정정 다른 파일 양립 (layout.rs×3 + paragraph_layout.rs + shape_layout.rs)
- sweep 169/169 same + 작업지시자 시각 판정 통과
- 원 Issue #952 의 5 분리 결함 완결 (#956/#958/#961/#963/#964)
- @jangster77 연속 5 PR 완결
… 박스 외부 텍스트 표시 해소 (closes #965, ports PR #918 Stage 33-A)

`samples/hwp3-sample16.hwp` page 18 (한컴 page 16) 의 WMF 다이어그램 (주전산센터 목표시스템 구성안) 내부 박스의 한글 텍스트 ("PE6450", "기록서버", "Windows 서버군", "Unix 서버군" 등) 가 박스 외부로 벗어남.

`src/wmf/converter/svg/mod.rs:2191-2197` 의 SetTextAlign vertical bits 파싱:

```rust
let align_vertical = [
    VerticalTextAlignmentMode::VTA_BOTTOM,
    VerticalTextAlignmentMode::VTA_TOP,    // VTA_TOP = 0x0000
]
.into_iter()
.find(|a| record.text_alignment_mode & (*a as u16) == *a as u16)
.unwrap_or(VerticalTextAlignmentMode::VTA_BASELINE);
```

`mode & VTA_TOP(=0x0000) == 0x0000` 가 **항상 true** → BASELINE/BOTTOM 인 mode 도 VTA_TOP 으로 잘못 매핑 → `ext_text_out` 에서 +ascent (~em × 0.8) shift → baseline 이 cell-top 보정만큼 아래로 → 박스 하단 라인 걸침.

WMF spec [MS-WMF] 2.1.2.18:
- TA_TOP = 0x0000 (default)
- TA_BOTTOM = 0x0008
- TA_BASELINE = 0x0018

`src/wmf/converter/svg/mod.rs` 3 영역 (~60 lines):

vertical bits (0x0018 mask) 값 기준 BASELINE → BOTTOM → TOP 우선순위 분기.

```rust
let v_bits = record.text_alignment_mode & 0x0018;
let align_vertical = if v_bits == 0x0018 {
    VerticalTextAlignmentMode::VTA_BASELINE
} else if v_bits == 0x0008 {
    VerticalTextAlignmentMode::VTA_BOTTOM
} else {
    VerticalTextAlignmentMode::VTA_TOP
};
```

```rust
match self.context_current.text_align_vertical {
    VerticalTextAlignmentMode::VTA_TOP => +ascent (em × 0.8)
    VerticalTextAlignmentMode::VTA_BOTTOM => -descent (em × 0.2)
    VerticalTextAlignmentMode::VTA_BASELINE => 0
    _ => 0,
}
```

기존 `font.height < 0 => -font.height` 잘못된 보정 제거.

META_TEXTOUT 동일 baseline 보정 (ext_text_out 와 일관성).

PR #918 (closed, 5082 additions) 의 Stage 33-A 핵심 height/baseline 보정만 단독 포팅. 제외:
- LibreOffice emfio 포팅, WASM RasterPlayer, nested SVG inline embed, woff2 base64 임베드 제거, DX byte-aware indexing, POLYPOLYGON fill-rule

PR #918 close 사유 (다양한 부작용) 회피 + root cause fix 만 도입. + text_out baseline 추가.

- cargo test --release --lib: 1288 passed, 0 failed
- sample16 page 18 WMF 박스 내부 한글 텍스트 정상 위치 ✓ 한컴 viewer 정합
- WMF sample (sample14 page 0~8, sample4 page 1) PNG diff <1% (정상화 방향, 회귀 없음)

| 영역 | 영향 |
|------|------|
| WMF BASELINE/BOTTOM 모드 텍스트 | 정상 위치 (회귀 fix) |
| WMF TOP 모드 텍스트 | 기존 동작 (영향 없음) |
| 비-WMF | 영향 없음 |
| WASM 환경 | 정합 개선 예상 (Canvas2D 가 동일 SVG 사용) |

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…gn vertical bits 파싱 정정

@jangster77 — Issue #965: sample16 page 18 WMF 다이어그램 박스 내부 한글 텍스트
박스 외부 벗어남.

Root cause (src/wmf/converter/svg/mod.rs:2208 set_text_align): mode & VTA_TOP(=0x0000)
== 0x0000 항상 true → BASELINE/BOTTOM mode 도 VTA_TOP 오매핑 → +ascent shift →
baseline 박스 하단 걸침. WMF [MS-WMF] 2.1.2.18 spec 정합.

본질 (svg/mod.rs 3 영역 ~60 lines):
- set_text_align (:2208): v_bits = mode & 0x0018 마스킹 + 우선순위 BASELINE→BOTTOM→TOP
- ext_text_out (:811): baseline y shift 정합 (VTA_BASELINE=0, BOTTOM=-em*0.2, TOP=+ascent)
  — font.height < 0 잘못된 보정 제거
- text_out (:1545): META_TEXTOUT 동일 보정 (PR #918 미포함, 본 PR 추가)

PR #918 supersede: PR #918 (CLOSED 5/16, +5082/-74 거대 PR, 다양한 부작용
LibreOffice emfio/WASM RasterPlayer 등) → Stage 33-A root cause ~60 lines 만
단독 포팅. feedback_pr_supersede_chain (a) + feedback_small_batch_release_strategy
권위 사례.

본 환경 충돌 수동 해결: orders/20260517.md (--ours + Task #965 작업 일지 갱신).
svg/mod.rs auto-merge — devel PR #860/#864 (5/16, EMF/WMF image 렌더) 변경
보존 + PR #966 정정 양립 확인.

자기 검증: cargo test --release --lib 1288 passed / clippy 통과 /
광범위 sweep 7 fixture / 169 페이지 / 169 same / 0 diff / WASM 4.4 MB 재빌드
시각 판정: 작업지시자 시각 검증 통과 (sample16 p18 WMF 박스 한글 텍스트 정상
+ WMF sample14/4 + devel PR #860/#864 EMF/WMF 회귀 부재)
CI: ✅ Build & Test + CodeQL

@jangster77 연속 5 PR (#956~#964) 완결 후 추가 #966 (#968 후속)
- mydocs/pr/archives/pr_966_review.md (WMF SetTextAlign vertical bits 분석)
- mydocs/pr/archives/pr_966_report.md (옵션 A + PR #918 supersede)
- mydocs/orders/20260517.md PR #966 행 추가

핵심:
- mode & VTA_TOP(=0) == 0 항상-true 버그 root cause
- WMF [MS-WMF] 2.1.2.18 spec 정합 (v_bits 0x0018 mask)
- PR #918 (CLOSED, +5082 거대 PR) → root cause ~60 lines 단독 포팅
- svg/mod.rs auto-merge (devel PR #860/#864 EMF/WMF image 렌더 보존)
- sweep 169/169 same + 작업지시자 시각 판정 통과
- Issue #965 close, 추가 PR #968 후속
… inflate 해소 (closes #967)

## 본질

`samples/hwp3-sample18.hwp` (HWP3) 페이지 수 rhwp 69 vs 한컴 67 — **+2 inflate**.

## Root cause

`dump-pages` 분석:
- Page 2: pi=27 "(빈)" 1개만 (24px), vpos=69356 HU
- Page 14: pi=164 "(빈)" 1개만 (25.6px), vpos=69836 HU
- pi=28, pi=165 (다음 paragraph) 에 [쪽나누기] (column_type::Page)

빈 paragraph 가 이전 page 잔여 공간 초과 (page 1: body 935px, used 934.2px, 빈 24px → over) → 별도 page 분기 → +1 inflate × 2.

`src/renderer/typeset.rs:555-584` 의 `next_will_vpos_reset` 가드:
```rust
let next_force_break = next_para.column_type == ColumnBreakType::Page
    || next_para.column_type == ColumnBreakType::Section;
if next_force_break {
    false   // ← 다음 force_break 시 false (hwp-multi-001 회귀 차단 목적)
}
```

→ pi=28/165 의 [쪽나누기] → next_force_break=true → next_will_vpos_reset=false → 단독 빈페이지 차단 가드 미발동 → pi=27/164 단독 page 생성.

## Fix

`src/renderer/typeset.rs:584-604`: 기존 next_will_vpos_reset 가드 직후 별도 분기 추가.

```rust
} else if !st.current_items.is_empty() && para_idx + 1 < paragraphs.len() {
    // [Task #967] 빈 paragraph 직후 force page break (쪽나누기) case 가드
    let next_para = &paragraphs[para_idx + 1];
    let next_force_break = next_para.column_type == ColumnBreakType::Page
        || next_para.column_type == ColumnBreakType::Section;
    let is_curr_empty = para.text.is_empty() && para.controls.is_empty();
    if next_force_break && is_curr_empty {
        continue;  // 빈 paragraph skip — 단독 page 차단
    }
}
```

기존 next_will_vpos_reset 의 next_force_break 제외 조건 (hwp-multi-001 회귀 차단) **보존**.

## 검증

- cargo test --release --lib: 1288 passed, 0 failed, 2 ignored
- sample18.hwp 페이지 수: 69 → 67 ✓ (한컴 정합)
- 다중 sample 회귀 검증 (16 sample):
  - sample, sample10/11/13/14/16/19/4/5, table_test*, multi-table-001/002, exam_kor/math/eng: 모두 변경 없음
  - **hwp-multi-001 (회귀 차단 case): 변경 없음 ✓** (기존 가드 보존 확인)

## 영향

| 영역 | 영향 |
|------|------|
| 빈 paragraph + 다음 [쪽나누기] | skip → +1 page inflate 제거 (회귀 fix) |
| 비-빈 paragraph + 다음 [쪽나누기] | 영향 없음 |
| 빈 paragraph + 다음 일반 paragraph | 영향 없음 (기존 가드) |
| hwp-multi-001 (회귀 차단) | 영향 없음 |

## 관련

- 닫힌 issue #927 — sample16 페이지 수 inflate (본 fix 와 무관)
- 잔존: HWPX sample18-hwp5.hwpx +7 inflate (별도 issue, HWPX 특화 pagination)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jangster77 — Issue #967: HWP3 sample18 페이지 수 rhwp 69 vs 한컴 67 (+2 inflate).
빈 paragraph (pi=27/164) 직후 [쪽나누기] (pi=28/165) → 빈 paragraph 별도
page 분기 → 단독 빈 페이지.

Root cause (typeset.rs:555-584): next_force_break (쪽나누기) 시
next_will_vpos_reset 가드 미발동 (hwp-multi-001 회귀 차단 목적) → 빈 paragraph
단독 page 생성.

본질 (src/renderer/typeset.rs:585 별도 분기):
next_force_break && is_curr_empty && empty_h_px > avail (overflow) 시에만
continue (skip → 단독 page 차단). fit 가능 시 정상 emit.

v2 정밀화: v1 (조건 무관 skip) → aift.hwp snapshot 회귀 → v2 (overflow 한정)
→ aift.hwp 18 case 정상 emit + sample18 fix. feedback_hancom_compat_specific_over_general
권위 사례 (일반화 위험 발견 후 케이스별 정밀화).

영역 좁힘: 빈 paragraph + 쪽나누기 + overflow 한정 — fit 가능/비-빈/일반
paragraph/hwp-multi-001 영향 없음.

본 환경 cherry-pick 충돌 0건 — typeset.rs auto-merge (devel Task #836 Endnote
:1064/1091 보존 + PR #968 :585 분기 양립). orders/20260518.md 신규 (5/18).
devel merge commit 2개 cherry-pick 제외 — 본질 3c1ea87 만.

자기 검증: cargo test --release 전체 (lib 1288 + integration svg_snapshot 8) ALL GREEN
+ cargo clippy --release -D warnings 통과 + 광범위 sweep 7 fixture / 169 페이지 /
169 same / 0 diff (aift.hwp 74 same — v1 회귀 해소 입증) + WASM 4.4 MB 재빌드.

⚠️ samples/hwp3-sample18.hwp 영역 영역 PR 미포함 + 본 환경 부재 — 작업지시자 결정
영역 영역 sweep 169/169 same + cargo test 전체 통과 영역 영역 회귀 부재 입증 +
sample18 페이지 수 69→67 정합 영역 영역 컨트리뷰터 PR 본문 신뢰 (작업지시자 승인).

CI: ✅ Build & Test + CodeQL + Canvas visual diff

@jangster77 PR 시리즈 완결 (연속 5 PR #956~#964 + #966 + #968)
- mydocs/pr/archives/pr_968_review.md (빈 paragraph + 쪽나누기 v2 정밀화 분석)
- mydocs/pr/archives/pr_968_report.md (옵션 A + fixture 부재 작업지시자 결정 + PR 시리즈 총평)
- mydocs/orders/20260518.md 신규 PR 처리 섹션 + @jangster77 PR 시리즈 총평

핵심:
- next_force_break 가드 미발동 root cause + v2 정밀화 (overflow 한정, aift.hwp 회귀 해소)
- typeset.rs auto-merge (devel Task #836 Endnote 보존 + PR #968 :585 분기 양립)
- cargo test 전체 + sweep 169/169 same + 기존 HWP3 샘플 시각 판정 통과
- samples/hwp3-sample18.hwp 부재 → 작업지시자 결정 회귀 부재 입증 + PR 본문 신뢰
- Issue #967 close, @jangster77 PR 시리즈 (7 PR #956~#968) 완결
- 잔존: HWPX sample18-hwp5 +7 inflate + sample18 fixture 추가 권장
버전 0.7.11 → 0.7.12 (Cargo.toml + rhwp-vscode/npm-editor/rhwp-studio package.json)
CHANGELOG 갱신 (CHANGELOG.md / CHANGELOG_EN.md / rhwp-vscode/CHANGELOG.md)
WASM 재빌드 산출물 동기화 (rhwp-studio/public/rhwp.js)

@jangster77 7-PR 시리즈 (#956~#968) + Issue #952 5-결함 완결 + WMF #966 + HWP3 #968 + LTO #818

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@edwardkim edwardkim merged commit 1899ef9 into main May 17, 2026
12 checks passed
edwardkim added a commit that referenced this pull request May 17, 2026
PR #970 (devel→main) merge + v0.7.12 태그 (1899ef9) 기록

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
edwardkim pushed a commit that referenced this pull request May 22, 2026
…t check 복구)

Task #1058 Stage 16 (adaa0b0) 에서 추가된 신규 파일이 rustfmt 정합
미통과 — devel CI #970 (sha=749048c9) Format check 실패. cargo fmt
적용으로 정정. 본 환경 cargo fmt --check 전체 exit 0 검증.

본 정정은 PR #1057 처리 진행 전 devel CI 회복 위한 단독 commit.
다른 처리분 (PR #1054/#1059 누적 + 예정 PR #1057) 은 별도 누적 push
계획 (작업지시자 결정 "본 PR 처리분 누적 후 push").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xogh3198
Copy link
Copy Markdown

"안녕하세요! 지난번 #799 제안이 v0.7.12 릴리즈에 성공적으로 반영된 것을 확인했습니다. 프로젝트에 도움이 되어 기쁩니다!

다만 제가 당시 에디터(Cursor)의 Git 이메일 설정을 누락하는 바람에, 수동으로 통합된 커밋에 제 깃허브 계정이 제대로 연동되지 않아 Contribution 기록(Contributors 명단 및 프로필 잔디)이 남지 않은 것을 뒤늦게 확인했습니다.

혹시 괜찮으시다면 제 깃허브 계정이 Contributors에 정식으로 기록될 수 있도록, README.md에 기여자 명단을 추가하는 PR이나 가벼운 문서 보완 PR을 하나 더 올려도 괜찮을까요?

@edwardkim
Copy link
Copy Markdown
Owner Author

"안녕하세요! 지난번 #799 제안이 v0.7.12 릴리즈에 성공적으로 반영된 것을 확인했습니다. 프로젝트에 도움이 되어 기쁩니다!

다만 제가 당시 에디터(Cursor)의 Git 이메일 설정을 누락하는 바람에, 수동으로 통합된 커밋에 제 깃허브 계정이 제대로 연동되지 않아 Contribution 기록(Contributors 명단 및 프로필 잔디)이 남지 않은 것을 뒤늦게 확인했습니다.

혹시 괜찮으시다면 제 깃허브 계정이 Contributors에 정식으로 기록될 수 있도록, README.md에 기여자 명단을 추가하는 PR이나 가벼운 문서 보완 PR을 하나 더 올려도 괜찮을까요?

기여자 명단에 누락된 점 사과드립니다. 기여자 명단에 @xogh3198 님을 추가하도록 하겠습니다.

@xogh3198
Copy link
Copy Markdown

xogh3198 commented May 29, 2026

Participant 등록해 주셔서 감사합니다.

확인해 보니 기존 PR 커밋 author가 thlee2 <thlee2>로 기록되어 있어,
GitHub 계정과 연결되지 않아
프로필 contribution레 반영되지 않는 것으로 확인했습니다

원인은 로컬 Git user.email 설정 오류였고 이메일설정을 수정하였습니다

이미 merge된 #571,#799 커밋의 author rewrite는 히스토리 변경 부담이 있어
요청드리지 않겠습니다.

대신 compare/이력 기능 관련 README·안내 문구 등
최소 수정 PR을 upstream/devel 기준으로 다시 올리겠습니다.

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.

9 participants