diff --git a/mydocs/manual/svg_regression_diff.md b/mydocs/manual/svg_regression_diff.md new file mode 100644 index 000000000..1b3587355 --- /dev/null +++ b/mydocs/manual/svg_regression_diff.md @@ -0,0 +1,85 @@ +# SVG 회귀 검증 도구 — `scripts/svg_regression_diff.sh` + +Layout 본질 변경 (#496 / #500 등) 시 광범위 SVG 회귀 검증을 자동화한다. Phase 1 (#517) 산출물. + +## 사용법 + +### Mode 1: 두 commit/branch 비교 (자동 빌드) + +```bash +scripts/svg_regression_diff.sh build [SAMPLES...] +``` + +각 ref 에서 `src/` 만 체크아웃 → release 빌드 → 7개 기본 샘플 export → byte 비교. + +예: +```bash +# devel 과 local/task500 비교 (모든 기본 샘플) +scripts/svg_regression_diff.sh build devel local/task500 + +# 특정 샘플만 비교 +scripts/svg_regression_diff.sh build devel HEAD exam_science exam_kor +``` + +작업 트리 보존: 시작 시 자동 stash, 종료 시 pop. + +### Mode 2: 두 디렉토리 비교 (이미 존재하는 SVG) + +```bash +scripts/svg_regression_diff.sh diff +``` + +각 디렉토리 구조: `//_NNN.svg` + +예: +```bash +scripts/svg_regression_diff.sh diff /tmp/before /tmp/after +``` + +## 출력 형식 + +``` +{sample}: total={N} same={M} diff={D} diff_pages=[페이지 목록] +--- +TOTAL: pages={total} same={same} diff={diff} +``` + +`diff > 0` 인 sample 의 `diff_pages` 에서 변경된 SVG 파일을 직접 비교 (`diff /tmp/svg_diff_before// /tmp/svg_diff_after//`) 하여 변경 내용 검토. + +## 기본 샘플 목록 + +(SAMPLES 미지정 시 사용) +- `exam_kor` (20 페이지) +- `exam_eng` (8 페이지) +- `exam_science` (4 페이지) +- `exam_math` (20 페이지) +- `synam-001` (35 페이지) +- `aift` (77 페이지) +- `2010-01-06` (6 페이지) + +총 170 페이지, 다양한 layout 패턴 (다단/표/수식/각주/인라인 컨트롤) 커버. + +## 활용 사례 + +### 신규 commit 의 회귀 검증 + +```bash +git commit -m "..." +scripts/svg_regression_diff.sh build HEAD~1 HEAD +``` + +`diff > 0` 페이지가 의도된 정정인지 확인 → 의도되지 않으면 회귀. + +### Phase 2~4 layout 본질 변경 검증 + +```bash +scripts/svg_regression_diff.sh build devel local/task +``` + +리팩터링 변경의 영향 범위를 정량 확인. + +## 관련 + +- Phase 1 #517 (본 도구 추가) +- Phase 0 로드맵: `mydocs/tech/layout_refactor_roadmap.md` +- `RHWP_LAYOUT_DEBUG=1` env logging — 같이 사용 시 결함 측정·재현 자동화 diff --git a/mydocs/plans/task_m100_517.md b/mydocs/plans/task_m100_517.md new file mode 100644 index 000000000..eb3408b8f --- /dev/null +++ b/mydocs/plans/task_m100_517.md @@ -0,0 +1,53 @@ +# Task #517 수행계획서 — Layout 리팩터링 Phase 1 + +**이슈**: #517 — Layout 리팩터링 Phase 1: 디버그 인프라 + 회귀 검증 도구 +**브랜치**: `local/task517` +**작성일**: 2026-05-02 + +## 1. 목적 + +#467 / #491 / #496 본질 정정 (Phase 2~4) 의 선결 인프라 추가. +- Layout 디버깅 도구로 결함 측정·재현 자동화 +- 회귀 검증 도구로 Phase 2~4 변경의 광범위 안전성 검증 + +코드 변경은 인프라/도구만 추가. 본질 동작 변경 없음. + +## 2. 단계 + +### Stage 1 — `RHWP_LAYOUT_DEBUG=1` env logging + +`layout_inline_table_paragraph` (paragraph_layout.rs:81) 에 env-var-checked eprintln 추가: +- paragraph 메타데이터: pi, col_x, col_w, y_start, ls_count +- line_seg 정보: vpos, lh, ls, text_start +- 인라인 표 정보: rows, cols, w, h, vert_align +- wrap 결정: ch_idx, current_y, reason (hwp_break / dynamic) +- TextRun 발행: x, y, text 일부 + +기존 `RHWP_TYPESET_DRIFT` 패턴 따름 (`src/renderer/typeset.rs:861`). + +### Stage 2 — 회귀 검증 도구 정형화 + +7-sample byte-diff 비교를 정형 script 로: +- `scripts/svg_regression_diff.sh` — before/after 디렉토리 + sample list 입력 → 페이지별 diff 출력 +- 기본 sample list: exam_kor, exam_eng, exam_science, exam_math, synam-001, aift, 2010-01-06 +- 결과 형식: `{sample}: total={N} same={M} diff={D}` + diff 파일 경로 + +매뉴얼: `mydocs/manual/svg_regression_diff.md` + +## 3. 검증 + +- Stage 1: `RHWP_LAYOUT_DEBUG=1` 로 exam_science p2 export → 로그 출력 확인. 단위 + 통합 테스트 통과 +- Stage 2: 7 샘플 before/after 비교 스크립트 실행 검증 + +## 4. 리스크 + +- Stage 1: env-var-checked 로깅이므로 기본 동작 변경 없음. 회귀 0 +- Stage 2: 별도 script 추가, 코드 변경 없음 + +## 5. 산출물 + +- `src/renderer/layout/debug.rs` (신규, 또는 paragraph_layout.rs 내 helper) +- `scripts/svg_regression_diff.sh` (신규) +- `mydocs/manual/svg_regression_diff.md` +- `mydocs/working/task_m100_517_stage{1,2}.md` +- `mydocs/report/task_m100_517_report.md` diff --git a/mydocs/report/task_m100_517_report.md b/mydocs/report/task_m100_517_report.md new file mode 100644 index 000000000..acabaa1cb --- /dev/null +++ b/mydocs/report/task_m100_517_report.md @@ -0,0 +1,73 @@ +# Task #517 최종 보고서 — Layout 리팩터링 Phase 1 + +**이슈**: #517 +**브랜치**: `local/task517` +**Phase**: 1 (디버그 인프라 + 회귀 검증 도구) + +## 1. 작업 내용 + +Phase 2~4 (본질 정정) 의 선결 인프라 추가. + +## 2. 변경 파일 + +| 파일 | 변경 | +|------|------| +| `src/renderer/layout/paragraph_layout.rs` | `layout_debug_enabled()` + `layout_inline_table_paragraph` 진단 로깅 (env-var-checked) | +| `scripts/svg_regression_diff.sh` | 신규 (130 LOC) — build/diff 두 모드 | +| `mydocs/manual/svg_regression_diff.md` | 사용 매뉴얼 | +| `mydocs/plans/task_m100_517.md` | 수행계획서 | +| `mydocs/working/task_m100_517_stage{1,2}.md` | 단계별 보고서 | +| 본 보고서 | | + +## 3. 검증 + +### 3-1. 기능 검증 + +- Stage 1 logging: exam_science p2 pi=61 (#496 재현) 에서 `ls_count=3 tables=1 rows=2` 등 핵심 정보 출력 확인 +- Stage 2 diff: 기존 /tmp/task500_{before,after} 비교에서 의도된 1페이지 정정 정확 식별 + +### 3-2. 회귀 검증 + +- `cargo test --release`: **1103 passed; 0 failed; 1 ignored** +- `scripts/svg_regression_diff.sh build devel HEAD`: **170/170 byte 동일, diff=0** + +env-var-checked 로깅 + 별도 script 만 추가 → 기본 동작 변경 없음. + +## 4. 영향 범위 + +| 케이스 | 영향 | +|--------|------| +| 기본 export-svg 동작 | 변화 없음 (env var 미지정 시 logging 미동작) | +| `RHWP_LAYOUT_DEBUG=1` 지정 시 | layout_inline_table_paragraph 진입 시 진단 로깅 출력 | +| 단위/통합 테스트 | 영향 없음 | + +## 5. 활용 + +### `RHWP_LAYOUT_DEBUG=1` + +```bash +RHWP_LAYOUT_DEBUG=1 ./target/release/rhwp export-svg samples/exam_science.hwp -p 1 2>&1 | grep "LAYOUT_" +``` + +### `scripts/svg_regression_diff.sh` + +```bash +# 두 commit 비교 +./scripts/svg_regression_diff.sh build devel local/task + +# 두 디렉토리 비교 +./scripts/svg_regression_diff.sh diff /tmp/before /tmp/after +``` + +## 6. 후속 + +Phase 2 (line_break_char_idx 다중화) 진행 시 본 인프라 사용: +- 변경 전후 `RHWP_LAYOUT_DEBUG=1` 로 결함 케이스 baseline 측정 비교 +- `svg_regression_diff.sh build devel local/taskN` 으로 170 페이지 광범위 회귀 검증 + +## 7. 요약 + +- Layout 디버깅 인프라 (`RHWP_LAYOUT_DEBUG=1`) ✓ +- 회귀 검증 도구 (`scripts/svg_regression_diff.sh`) ✓ +- 회귀 0건 (170/170 byte 동일, 1103 단위 테스트 통과) ✓ +- Phase 2~4 진행을 위한 도구 기반 마련 ✓ diff --git a/mydocs/working/task_m100_517_stage1.md b/mydocs/working/task_m100_517_stage1.md new file mode 100644 index 000000000..cd6e6c4fb --- /dev/null +++ b/mydocs/working/task_m100_517_stage1.md @@ -0,0 +1,54 @@ +# Task #517 Stage 1 보고서 — RHWP_LAYOUT_DEBUG env logging + +**이슈**: #517 +**브랜치**: `local/task517` +**Stage**: 1 / 2 + +## 1. 변경 내용 + +`src/renderer/layout/paragraph_layout.rs`: +- `layout_debug_enabled()` helper 추가 (env var 체크) +- `layout_inline_table_paragraph` 시작 부분에 진단 로깅 (env-var-checked) + +## 2. 출력 형식 + +``` +LAYOUT_INLINE_TABLE_PARA: pi={N} sec={S} col_x={X} col_w={W} y_start={Y} y={Y'} sb={SB} sa={SA} ml={ML} mr={MR} align={A} ls_count={N} tables={T} + LAYOUT_LS[i]: vpos={V} lh={LH} ls={LS} bl={BL} text_start={TS} sw={SW} + LAYOUT_INLINE_TBL[i]: ctrl_idx={CI} rows={R} cols={C} w={W} h={H} vert={V} horz={H} wrap={WR} +``` + +## 3. 검증 + +### 3-1. exam_science p2 pi=61 (#496 재현 케이스) + +``` +LAYOUT_INLINE_TABLE_PARA: pi=61 sec=0 col_x=534.8 col_w=422.6 y_start=1176.8 y=1176.8 sb=0.0 sa=6.7 ml=15.1 mr=0.0 align=Justify ls_count=3 tables=1 + LAYOUT_LS[0]: vpos=74118 lh=2864 ls=460 bl=1432 text_start=0 sw=18939 + LAYOUT_LS[1]: vpos=77442 lh=1150 ls=460 bl=575 text_start=13 sw=18939 + LAYOUT_LS[2]: vpos=79052 lh=1150 ls=460 bl=575 text_start=60 sw=30562 + LAYOUT_INLINE_TBL[0]: ctrl_idx=0 rows=2 cols=1 w=14745 h=2864 vert=Top horz=Left wrap=TopAndBottom +``` + +핵심 정보: +- ls_count=3 (다중 줄 paragraph) +- table rows=2 (다중행 인라인 표) +- ls[2].text_start=60 (HWP 인코딩 break 위치) + +→ #496 결함 분석에 직접 활용 가능. + +### 3-2. 회귀 검증 + +`scripts/svg_regression_diff.sh build devel HEAD` 결과: +- TOTAL: pages=170 same=170 diff=0 + +env-var-checked 로깅이므로 기본 동작 변경 없음. **회귀 0건**. + +### 3-3. 단위 + 통합 테스트 + +- `cargo test --release --lib`: 1103 passed +- `cargo test --release --tests`: 모든 통과 + +## 4. 잔여 + +Stage 2 (회귀 검증 도구 정형화) 로 진행. diff --git a/mydocs/working/task_m100_517_stage2.md b/mydocs/working/task_m100_517_stage2.md new file mode 100644 index 000000000..7796dcd0c --- /dev/null +++ b/mydocs/working/task_m100_517_stage2.md @@ -0,0 +1,77 @@ +# Task #517 Stage 2 보고서 — 회귀 검증 도구 정형화 + +**이슈**: #517 +**브랜치**: `local/task517` +**Stage**: 2 / 2 + +## 1. 변경 내용 + +- `scripts/svg_regression_diff.sh` (신규, 약 130 LOC bash) +- `mydocs/manual/svg_regression_diff.md` (사용 매뉴얼) + +## 2. 기능 + +### Mode 1: `build [SAMPLES...]` + +- 두 commit/branch 에서 자동 빌드 → SVG 추출 → byte 비교 +- 작업 트리 자동 stash/pop +- `/tmp/svg_diff_{before,after}/` 에 결과 보존 + +### Mode 2: `diff ` + +- 이미 존재하는 두 디렉토리 비교 (재빌드 없이) + +## 3. 출력 형식 + +``` +{sample}: total={N} same={M} diff={D} diff_pages=[페이지 목록] +--- +TOTAL: pages={total} same={same} diff={diff} +``` + +## 4. 검증 + +### 4-1. Mode 2 (diff): 기존 #500 작업 dir 비교 + +``` +$ ./scripts/svg_regression_diff.sh diff /tmp/task500_before /tmp/task500_after +2010-01-06: total=6 same=6 diff=0 +aift: total=77 same=77 diff=0 +exam_eng: total=8 same=8 diff=0 +exam_kor: total=20 same=20 diff=0 +exam_math: total=20 same=20 diff=0 +exam_science: total=4 same=3 diff=1 diff_pages=[exam_science_002.svg] +synam-001: total=35 same=35 diff=0 +--- +TOTAL: pages=170 same=169 diff=1 +``` + +→ #500 fix 의 의도된 1페이지 정정 정확히 식별. + +### 4-2. Mode 1 (build): devel vs HEAD (Phase 1 자체) + +``` +$ ./scripts/svg_regression_diff.sh build devel HEAD +TOTAL: pages=170 same=170 diff=0 +``` + +→ Phase 1 변경 자체의 회귀 0건 검증. + +## 5. 활용 + +- 신규 commit 회귀 검증 +- Phase 2~4 layout 본질 변경 광범위 회귀 검증 (170 페이지 자동 비교) +- `RHWP_LAYOUT_DEBUG=1` 와 함께 사용 시 결함 측정·재현 자동화 + +## 6. 기본 샘플 커버리지 + +| 샘플 | 페이지 | 패턴 | +|------|--------|------| +| exam_kor | 20 | 다단, 헤더/푸터, master, 인라인 표/도형 | +| exam_eng | 8 | 영문 문제지 | +| exam_science | 4 | 과학 (수식/표/그림 다수) | +| exam_math | 20 | 수학 (수식 집중) | +| synam-001 | 35 | 일반 문서 | +| aift | 77 | 긴 문서 (다양한 패턴) | +| 2010-01-06 | 6 | 기본 케이스 | +| **합계** | **170** | 광범위 layout 커버 | diff --git a/scripts/svg_regression_diff.sh b/scripts/svg_regression_diff.sh new file mode 100755 index 000000000..14c37c026 --- /dev/null +++ b/scripts/svg_regression_diff.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# scripts/svg_regression_diff.sh — Layout 변경 회귀 검증 (Phase 1 #517) +# +# 두 commit 또는 두 디렉토리의 SVG 출력을 페이지별로 byte 비교한다. +# Layout 본질 변경 (Phase 2~4) 시 광범위 회귀 검증에 사용. +# +# 사용법: +# scripts/svg_regression_diff.sh build [SAMPLES...] +# — BEFORE_REF/AFTER_REF (commit/branch) 에서 빌드 → /tmp/svg_diff_{before,after}/ 에 출력 → 비교 +# scripts/svg_regression_diff.sh diff +# — 이미 존재하는 두 디렉토리만 비교 +# +# 기본 SAMPLES (지정 안 하면 사용): +# exam_kor exam_eng exam_science exam_math synam-001 aift 2010-01-06 +# +# 출력: +# {sample}: total={N} same={M} diff={D} +# diff 발생 시 페이지 목록 + diff 파일 경로 + +set -euo pipefail + +DEFAULT_SAMPLES=(exam_kor exam_eng exam_science exam_math synam-001 aift 2010-01-06) +ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +usage() { + sed -n '2,/^$/p' "$0" | sed 's/^# //;s/^#//' + exit 1 +} + +build_at_ref() { + local ref="$1" + local out_dir="$2" + shift 2 + local samples=("$@") + + pushd "$ROOT" >/dev/null + + # 빌드 (현재 작업 트리 보존) + if ! git diff --quiet || ! git diff --cached --quiet; then + echo " [build] working tree dirty, stashing..." >&2 + git stash push -m "svg_regression_diff_$ref" >&2 + local stashed=1 + else + local stashed=0 + fi + + git checkout "$ref" -- src/ >&2 + cargo build --release >&2 + + # SVG 추출 + mkdir -p "$out_dir" + for s in "${samples[@]}"; do + local hwp="samples/${s}.hwp" + if [ ! -f "$hwp" ]; then + echo " [build] skip $s (no $hwp)" >&2 + continue + fi + mkdir -p "$out_dir/$s" + ./target/release/rhwp export-svg "$hwp" -o "$out_dir/$s/" >/dev/null 2>&1 || true + local n=$(ls "$out_dir/$s/" 2>/dev/null | wc -l | tr -d ' ') + echo " [build:$ref] $s: $n pages" >&2 + done + + # 작업 트리 복구 + git checkout HEAD -- src/ >&2 + if [ "$stashed" = "1" ]; then + git stash pop >&2 || true + fi + + popd >/dev/null +} + +diff_dirs() { + local before="$1" + local after="$2" + + local total_pages=0 + local total_same=0 + local total_diff=0 + + for d in "$before"/*/; do + local s=$(basename "$d") + local b="$before/$s" + local a="$after/$s" + [ -d "$a" ] || { echo "$s: SKIP (no $a)"; continue; } + + local total=0 same=0 diff=0 + local diff_pages=() + for f in "$b"/*.svg; do + [ -f "$f" ] || continue + local base=$(basename "$f") + total=$((total+1)) + if [ -f "$a/$base" ] && cmp -s "$f" "$a/$base"; then + same=$((same+1)) + else + diff=$((diff+1)) + diff_pages+=("$base") + fi + done + + printf '%s: total=%d same=%d diff=%d' "$s" "$total" "$same" "$diff" + if [ "$diff" -gt 0 ]; then + printf ' diff_pages=[%s]' "${diff_pages[*]}" + fi + echo + + total_pages=$((total_pages + total)) + total_same=$((total_same + same)) + total_diff=$((total_diff + diff)) + done + + echo "---" + echo "TOTAL: pages=$total_pages same=$total_same diff=$total_diff" +} + +main() { + [ $# -lt 1 ] && usage + + local cmd="$1" + shift + + case "$cmd" in + build) + [ $# -lt 2 ] && usage + local before_ref="$1" + local after_ref="$2" + shift 2 + local samples=("$@") + [ ${#samples[@]} -eq 0 ] && samples=("${DEFAULT_SAMPLES[@]}") + + local before_dir="/tmp/svg_diff_before" + local after_dir="/tmp/svg_diff_after" + rm -rf "$before_dir" "$after_dir" + + echo "=== Building $before_ref → $before_dir ===" + build_at_ref "$before_ref" "$before_dir" "${samples[@]}" + echo + echo "=== Building $after_ref → $after_dir ===" + build_at_ref "$after_ref" "$after_dir" "${samples[@]}" + echo + echo "=== Comparing ===" + diff_dirs "$before_dir" "$after_dir" + ;; + diff) + [ $# -lt 2 ] && usage + diff_dirs "$1" "$2" + ;; + *) + usage + ;; + esac +} + +main "$@" diff --git a/src/renderer/layout/paragraph_layout.rs b/src/renderer/layout/paragraph_layout.rs index 98fcc1c08..226822c6e 100644 --- a/src/renderer/layout/paragraph_layout.rs +++ b/src/renderer/layout/paragraph_layout.rs @@ -15,6 +15,13 @@ use super::text_measurement::{resolved_to_text_style, estimate_text_width, compu use super::border_rendering::create_border_line_nodes; use super::utils::{resolve_numbering_id, expand_numbering_format, numbering_format_to_number_format, find_bin_data, extract_shape_transform}; +/// `RHWP_LAYOUT_DEBUG=1` 로 활성화되는 layout 디버그 로깅 여부. +/// Phase 1 (#517) — 본질 정정 (#467/#491/#496) 시 결함 측정·재현 자동화에 사용. +#[inline] +pub(crate) fn layout_debug_enabled() -> bool { + std::env::var("RHWP_LAYOUT_DEBUG").map(|v| v == "1").unwrap_or(false) +} + /// lineseg baseline_distance를 폰트 어센트 기준으로 보정한다. /// CENTER 문단 수직정렬 등으로 baseline이 50% 이하로 설정된 경우, /// 텍스트 어센트(~80%)가 줄 박스 밖으로 넘치지 않도록 보장한다. @@ -116,6 +123,31 @@ impl LayoutEngine { }) .collect(); + // [Task #517 Stage 1] RHWP_LAYOUT_DEBUG 진단 로깅 + if layout_debug_enabled() { + eprintln!( + "LAYOUT_INLINE_TABLE_PARA: pi={} sec={} col_x={:.1} col_w={:.1} y_start={:.1} y={:.1} sb={:.1} sa={:.1} ml={:.1} mr={:.1} align={:?} ls_count={} tables={}", + para_index, section_index, col_area.x, col_area.width, y_start, y, + spacing_before, spacing_after, margin_left, margin_right, alignment, + para.line_segs.len(), inline_tables.len(), + ); + for (li, seg) in para.line_segs.iter().enumerate() { + eprintln!( + " LAYOUT_LS[{}]: vpos={} lh={} ls={} bl={} text_start={} sw={}", + li, seg.vertical_pos, seg.line_height, seg.line_spacing, + seg.baseline_distance, seg.text_start, seg.segment_width, + ); + } + for (ti, (ci, tbl)) in inline_tables.iter().enumerate() { + eprintln!( + " LAYOUT_INLINE_TBL[{}]: ctrl_idx={} rows={} cols={} w={} h={} vert={:?} horz={:?} wrap={:?}", + ti, ci, tbl.row_count, tbl.col_count, + tbl.common.width, tbl.common.height, + tbl.common.vert_align, tbl.common.horz_align, tbl.common.text_wrap, + ); + } + } + // 3. char_offsets 갭 분석으로 텍스트 세그먼트 분할 // 확장 컨트롤은 8 UTF-16 코드 유닛을 차지 let text_chars: Vec = para.text.chars().collect();