Skip to content

Task #352: dash 시퀀스 Justify 폭 부풀림 — leader-aware advance + elastic 분배#415

Closed
planet6897 wants to merge 40 commits into
edwardkim:develfrom
planet6897:local/task352
Closed

Task #352: dash 시퀀스 Justify 폭 부풀림 — leader-aware advance + elastic 분배#415
planet6897 wants to merge 40 commits into
edwardkim:develfrom
planet6897:local/task352

Conversation

@planet6897
Copy link
Copy Markdown
Contributor

배경

samples/exam_eng.hwp 5 페이지 32번 문항 빈칸 라인:

__________________________ of being 'stimulus-driven',

29개 ASCII 하이픈으로 구성된 빈칸이 PDF 대비 ~3.5 배 부풀어 후속 텍스트 'stimulus-driven', 의 마지막 콤마가 우측 단 경계를 넘어 잘림.

증상 (수정 전):

  • dash advance: 12.11 px/dash (PDF: ~7.4 px)
  • 29 dash 시퀀스 폭: 351 px (PDF: ~218 px)
  • of being 시작 x: 953 (단 우측 끝, 잘림 발생)

근본 원인

1. HY신명조 폰트 메트릭의 ASCII 하이픈 폭

src/renderer/font_metrics_data.rs:3848FONT_276_LATIN_0[13] (HY신명조 ASCII '-') 가 853/1024 em ≈ 0.83 em 으로 비정상적으로 넓음:

static FONT_276_LATIN_0: [u16; 95] = [341, 426, 426, 853, 640, 938, 853, 256,
512, 512, 512, 853, 298, 853, 298, ...
//                                ^ index 13 (0x2D '-') = 853

비교: 콤마(0x2C)=298, 마침표(0x2E)=298 — 일반 좁은 구두점 대비 dash 만 ~3 배 넓음.

2. Justify Branch A 의 공백 분배 부작용

29 dash × 12.11 px = 351 px 자연 폭으로 라인이 사용 가능 폭(408 px) 초과. Justify Branch A 가 음수 슬랙을 공백 압축으로 회수하려 하나 min-clamp(-2.55 px × 3 spaces) 로 제한되어 약 153 px 잘림 발생.

이슈 본문이 추정한 "Justify Branch B 가산" 가설(공백이 없는 라인의 글자 분배)은 틀림interior_spaces=3 이 정상 인식되어 Branch A 발동. 진짜 원인은 메트릭 자체.

3. Stage 2/3 좁은 advance 적용 후 발생한 2 차 회귀

dash advance 를 좁히자 라인에 양수 슬랙이 발생, Branch A 가 슬랙을 공백에 균등 분배하면서 단어 사이 공백이 자연 폭의 2~3 배로 팽창 (작업지시자 피드백 "Q32 이후 폭이 2배"). Stage 5 에서 PDF 의 elastic leader 동작 모방으로 해소.

4. PDF 의 dash 처리 방식 확인

pdftotext -bbox-layout -f 6 -l 6 samples/exam_eng.pdf 결과 PDF Q34 라인:

"34. Centralized, formal rules can"  (xMax=265.02 pt)
"."                                   (xMin=402.24 pt, 그 사이 137 pt)
─ 그 사이 영역의 단어 0개

dash 시퀀스가 단어로 추출되지 않는 것은 PDF 가 dash 를 path/line graphic 으로 직접 그렸다는 강한 증거. 우리 Stage 3 의 SVG <line> 대체 전략이 PDF 와 동일.

PDF dash advance 가변성 측정:

라인 dash 수 영역 폭 per dash em
Q32 29 218 px 7.49 px 0.49
Q34 40 183 px 4.57 px 0.30

→ PDF 는 dash leader 를 라인 슬랙에 따라 가변 폭으로 elastic 하게 채움.

변경

Stage 1 — 원인 확정 (428b01d)

paragraph_layout.rs:996text_measurement.rs:209 에 임시 eprintln! (RHWP_DEBUG_352 환경변수 게이트) 삽입하여 다음 확인:

  • Q32 라인 코드포인트 = [45 × 29, 32, "of being…"] (ASCII U+002D 29 개)
  • interior_spaces=3 ≠ 0 → Branch A 발동 (Branch B 아님)
  • text_w=561.00, avail=408.21 → 자연 폭 단계에서 153 px 초과
  • HY신명조 dash embedded=Some(12.747) (= 0.833 em)

문서: mydocs/troubleshootings/issue_352_root_cause.md + Stage 1 보고서.

Stage 2 — leader-aware 좁은 advance (69e420b)

src/renderer/layout/text_measurement.rs:

fn is_dash_leader_run(chars: &[char], i: usize) -> bool {
    if chars[i] != '-' { return false; }
    // i 좌·우로 같은 dash 가 연속한 길이 카운트, ≥3 이면 true
}

char_width 클로저 (estimate_text_width, compute_char_positions, estimate_text_width_unrounded) 에 적용:

let base_w = if is_dash_leader_run(&chars, i) {
    base_w_raw.min(font_size * 0.3)  // 0.3 em 으로 강제 좁힘
} else {
    base_w_raw
};

≥ 3 연속 조건 덕분에 단발 dash(예: "stimulus-driven", "32.-") 영향 없음.

Stage 3 — dash run 시각 라인 통합 (2248752)

src/renderer/svg.rs (line 1882~) + src/renderer/web_canvas.rs (line 1284~):

is_middle_dot 패턴(svg.rs:1924) 모방하여 ≥ 3 연속 dash 클러스터를 감지, 글리프 <text> 출력 스킵 + 단일 <line> 으로 통합 렌더 (underline 부재 시).

// dash leader run 감지 (cluster 인덱스 기준)
let dash_run_groups: Vec<(usize, usize)> = ...;
let suppress_dash_leader_line = !matches!(style.underline, UnderlineType::None);

for (cluster_idx, ...) in clusters.iter().enumerate() {
    if let Some((x1_rel, x2_rel)) = cluster_in_dash_run(cluster_idx) {
        if x1_rel.is_finite() && !suppress_dash_leader_line {
            // 첫 cluster 위치에서 단일 <line> 출력
        }
        continue; // 글리프 출력 스킵
    }
    ...
}

dash 글리프 자체 폭(scale 0.95 후 ~5.4 px) 이 advance(~4.4 px) 를 넘어 시각상 겹치는 문제도 동시 해결.

Stage 4 — 폭 미세 보정 (7f45fc0 + 037cba6)

작업지시자 피드백 ("폭이 조금 짧음 / 1.5 정도") 반영하여 PDF 직접 실측. 0.3 em (134.94 px) → 0.5 em (210.85 px) 로 조정. PDF 218 px 의 96.7% 일치.

Stage 5 — dash leader elastic Justify 분배 (6926c4b)

작업지시자 피드백 ("Q32 이후 폭이 2배 커짐") 의 진짜 원인 (단어 공백 팽창) 해결.

src/renderer/mod.rs — TextStyle 신규 필드:

/// Task #352: dash leader 글자당 추가 간격 (px). PDF elastic leader 모방.
pub extra_dash_advance: f64,

src/renderer/layout/text_measurement.rs — 세 char_width 클로저 갱신:

let is_leader = is_dash_leader_run(&chars, i);
let base_w = if is_leader { base_w_raw.min(font_size * 0.3) } else { base_w_raw };
let mut w = base_w * ratio + style.letter_spacing + style.extra_char_spacing;
if c == ' ' { w += style.extra_word_spacing; }
if is_leader { w += style.extra_dash_advance; }  // ← 추가

(Stage 4 의 0.5 em 은 0.3 em 좁은 base 로 회수, elastic extra 로 PDF 수준 폭 도달)

src/renderer/layout/paragraph_layout.rs Justify 분기:

let count_dash_leaders = |chars: &[char]| -> usize { ... };

let (extra_word_sp, extra_char_sp, extra_dash_sp) = if needs_justify {
    ...
    let leader_dashes = count_dash_leaders(&all_chars[..visible_count]);
    if interior_spaces > 0 {
        let slack = available_width - effective_used;
        if leader_dashes > 0 && slack > 0.0 {
            // dash 가 슬랙 흡수, 공백 자연 폭 유지
            (0.0, 0.0, slack / leader_dashes as f64)
        } else {
            // 기존: 공백 분배 또는 압축
            ...
        }
    } else if total_char_count > 1 {
        // 공백 없는 라인 (CJK 등) 도 leader_dashes 우선
        ...
    }
};

text_style.extra_dash_advance = extra_dash_sp;  // run 별 적용

세 분기 (Branch A, Branch B, Distribute, overflow, cell underflow) 의 반환 튜플을 (extra_word_sp, extra_char_sp, extra_dash_sp) 3-tuple 로 확장.

검증

Q32 측정 (s0 p221 L6, 5 페이지)

항목 v0 (수정 전) v1 (Stage 2) v2 (Stage 3) v3 (Stage 4) v4 (Stage 5) PDF 목표
dash advance 12.11 px 4.36 px 4.36 px 7.27 px 7.06 px ~7.4 px
29 dash 폭 351 px 126 px 126 px 211 px 204.7 px ~218 px
dash 글리프 29 개 29 개 0 개 (라인 대체) 0 개 0 개 0 개 (PDF 도 graphic)
, 우측 잘림 발생 해소 해소 해소 해소 정상

p6 Q33 라인 단어 공백 회복 (Stage 5 핵심 회귀 해소)

y=414 의 "to be free, then" 13 글자 폭:

단계 비고
Baseline (devel) 83.6 px 자연 폭 압축 (Branch A min-clamp)
Stage 3 (0.5 em) 125.7 px 공백 +14~24 px 팽창 (사용자 컴플레인)
Stage 5 (elastic) 95.6 px 자연 폭 (압축만 해제)

Stage 3 의 공백 폭증이 완전히 해소되고, 베이스라인 대비 +12 px 차이는 압축 해제 결과 (자연 폭 회복).

Stage 5 디버그 로그 (Q32)

[#352-S5] s0p221 L6 leader_dashes=29 interior_sp=3 text_w=337.00
          effective_used=330.00 avail=408.21 slack=78.21 per_dash_extra=2.697

각 dash advance = 0.3 em × 0.95 + 2.70 = 4.36 + 2.70 = 7.06 px
29 dashes 폭 = 29 × 7.06 = 204.7 px ✓ (SVG underline x1=597.12, x2=801.84)

cargo test --release

전 테스트 스위트 100% 통과:

  • lib: 1023 passed, 0 failed, 1 ignored
  • svg_snapshot: 6 passed
  • tab_cross_run: 1 passed
  • 기타 통합 50+ passed

영향 범위 점검

문서 전체 dash 통계 (Stage 1 측정): HY신명조 dash 1017 개, Times New Roman 295 개. is_dash_leader_run 의 ≥ 3 조건 덕분에 단발 dash 미영향:

샘플 dash 글리프 (수정 후) 비고
exam_eng 366 단발 dash 보존
exam_kor 440 정상
exam_math_8 1 정상
aift 460 정상
biz_plan 13 정상

비포함 / 백로그

  • HWP cs (line_seg.cs) 필드 활용 — 별 이슈 (현재 미사용 확인)
  • (U+2013), (U+2014) leader 처리 — 본 이슈 범위 외, 필요시 is_dash_leader_run 에 추가 가능
  • _____ underscore leader 일반화 — 우선순위 낮음
  • font_metrics_data 의 HY신명조 dash 메트릭 자체 보강 — TTF 실제값 검증 필요, 현 leader-aware 우회로 충분

커밋 목록

6926c4b Task #352 Stage 5: dash leader elastic Justify 분배 (PDF 모방)
037cba6 Task #352 폭 보정: 0.32 em → 0.5 em (PDF 실측 반영)
7f45fc0 Task #352 Stage 4: 최종 결과 보고서 + orders 갱신
2248752 Task #352 Stage 3: dash run 시각 라인 통합 (underline 일치)
69e420b Task #352 Stage 2: dash advance 자연 폭 보정 (leader-aware)
428b01d Task #352 Stage 1: 원인 확정 (HY신명조 dash 메트릭 853/1024 em)

closes #352

- row_block_start/end 필드 + compute_row_blocks 헬퍼
- row_block_for / snap_to_block_boundary 메서드
- 신규 단위 테스트 7개 (rowspan 단일/겹침/비인접 + 폴백 + 스냅)

회귀 검증용 샘플 hwpx/pdf 동반 커밋.
- pagination/engine.rs::split_table_rows: pre-loop first_block_h, snap_to_block_boundary, cur/next 블록 단일성 가드
- typeset.rs::paginate_table: 동일 패턴 적용 (실제 SVG 내보내기 경로)
- 다중 행 블록이 페이지에 들어가지 않으면 블록 전체를 한 단위로 배치

본 샘플 검증: 1쪽에서 표 분할 사라지고 2쪽에 표 전체 시작.
cargo test --lib: 1023 passed.
- cargo test --tests: 1073 passed (lib 1023 + integration 50)
- svg_snapshot 골든 6건 통과 (table-text, issue-147/157/267, form-002, deterministic)
- cargo build --release 성공
- 본 샘플 외 다른 표 샘플 (table-vpos-01, 표-텍스트) 정상

closes edwardkim#398
같은 paragraph 안에 TAC 컨트롤이 2개 이상 있을 때 두 번째 이후
그림의 pic_y가 paragraph 시작 y로 고정되어 표와 겹침.

- pi=51 ci=0 (단독 그림): pic_y=94.49 (정상, 선행 TAC 없음)
- pi=57 ci=1 (Table 뒤 그림): pic_y=578.09 (버그, y_offset=919.40 사용해야)

선행 TAC 존재 여부가 핵심 판별 조건임을 확인. Stage 2 구현 방향 확정.
같은 paragraph에 TAC 컨트롤(표/그림/도형) 2개 이상이 서로 다른 line_seg에
배치된 경우, 두 번째 이후 inline 그림이 첫 번째와 같은 y 좌표에 그려져
겹침/오버플로 발생하던 문제 수정.

- layout.rs::layout_shape_item: 선행 TAC 컨트롤이 있으면 para_start_y를
  진행된 y_offset으로 갱신하여 그림 y 좌표를 표 아래로 정확히 배치.
- typeset.rs::typeset_table_paragraph: 선행 TAC 그림의 line_seg 높이를
  current_height에 누적하고, 페이지 초과 시 다음 페이지로 분할.

기본 페이지네이션 엔진은 typeset.rs(TypesetEngine). engine.rs는 현재
RHWP_USE_PAGINATOR=1 fallback 경로이므로 typeset.rs만 수정.

회귀 테스트: 1023 passed, 0 failed.
샘플 비교: 7쪽 표 + 파이 차트 겹침 해소, 파이 차트가 8쪽 정상 배치.
closes edwardkim#402

검증 결과:
- 7쪽: 표 + 파이 차트 겹침 해소 (PDF 일치)
- 8쪽: 파이 차트 정상 배치
- cargo test 1023 passed, 0 failed
- 10개 대표 샘플 LAYOUT_OVERFLOW 카운트 회귀 없음
- 페이지 수 27→30 (분할로 인한 정상 증가)
기존 task_404.md(영문 줄바꿈 역공학 미실행 초안)는 archives로 이동.
신규 task_404 = orphan heading vpos 기반 분할 추가.
TypesetState.page_first_vpos 필드 추가 + typeset_section 메인 루프에
진단 로그 삽입. 타겟 샘플 분석 결과:

- pi=83 가설 확정: vpos overflow=886 HU, curr_h=906.6/avail=933.5
  (cumulative fit) + next pi=84 표 190.9px fit 불가 → orphan
- False positive 41건 중 40건은 wrap-around 페이지에서 vpos↔px 비율
  어긋남 (페이지 8 pi=57 TAC 그림 + 빈 문단 19개)

Stage 2 전략 재정의:
heading-orphan 패턴 (current fit + next block 못 fit + vpos overflow
+ single column) 4조건 모두 만족 시 push. 단순 vpos overflow check은
다수 회귀 위험.
typeset_section 메인 루프에 vpos 기반 보정 추가. 5개 조건 AND
(current fits + vpos overflow + next substantial + next doesn't
fit + single column non-wrap) 으로 false positive 차단.

설계 변경: page_top_vpos 는 TypesetState 필드 대신 current_items
첫 item 의 para_index 로 즉시 계산 (typeset_paragraph 내부 페이지
flush 와 동기 안 되는 문제 회피).

검증:
- pi=83 heading 이 페이지 9 → 페이지 10 으로 이동 (pi=84/85 표와
  함께 배치)
- 1073개 테스트 모두 통과
- 10개 샘플 LAYOUT_OVERFLOW: 회귀 없음 + 타겟 -15, kps-ai -1 개선
SVG 시각 검증으로 pi=83 "(7) 다수 기부자 현황" 이 페이지 10 첫
본문 라인으로 이동하고 후속 표 pi=84/85 와 함께 배치됨을 확인.

검증 기준 충족:
1. 페이지 9 SVG 에 pi=83 heading 미표시
2. 페이지 10 SVG 가 pi=83 + pi=84/85 표 함께 표시
3. 회귀 테스트 1073개 모두 통과
4. 10개 샘플 LAYOUT_OVERFLOW 회귀 없음 + 2개 샘플 개선

closes edwardkim#404
21페이지 2x1 표가 차트 아래로 ~400px 밀려나는 결함 분석.
TopAndBottom Picture(vert=Para) 다음 문단의 vpos 보정에서
lazy_base 산출 시 차트 높이가 이중 반영되는 문제 진단.
prev_has_overlay_shape 가드를 Control::Picture (non-TAC) +
TopAndBottom/vert=Para 케이스로 확장하는 3단계 계획 수립.
차트 그림(pi=172, 170×111mm, TopAndBottom, vert=Para) 다음 문단의
vpos 보정에서 lazy_base가 차트 높이(31470 HU)만큼 낮게 산출되어
pi=174 (2x1 표) 가 y=948 → 표 자체 높이 합산 후 1049.7 로 밀려나
LAYOUT_OVERFLOW 21.8px 발생. 후속 17개 빈 문단(pi=175~191)도
연쇄 overflow + pi=192 (10x5 표) overflow 521.7px.

베이스라인: 1023 lib + 6 svg_snapshot 통과, LAYOUT_OVERFLOW 19건.
…opAndBottom/vert=Para)

기존 가드는 Control::Shape + InFrontOfText|BehindText 만 검사하여
Picture (그림) 컨트롤과 TopAndBottom 케이스를 처리하지 못함.

Picture (non-TAC) 분기 추가 + TopAndBottom + vert_rel_to=Para 케이스
포함하도록 확장. 한컴이 후속 문단 vpos에 개체 높이를 반영하는 경우
sequential y_offset 이 이미 개체 바닥까지 진행된 상태에서
lazy_base 산출 시 prev_pi 텍스트 vpos_end 만 쓰면 차트 높이만큼
이중 점프 발생.

21페이지 LAYOUT_OVERFLOW 19건 -> 1건 (잔여는 별개 페이지네이션 결함).
2x1 표가 차트 바로 아래로 정상 위치 (PDF 21페이지와 일치).
1023 lib + 6 svg_snapshot 통과, 6개 샘플 무회귀.
cargo test --release 전체 10개 스위트 100% 통과
(1023 lib + 6 svg_snapshot + 통합 테스트, 실패 0).

10개 샘플 LAYOUT_OVERFLOW 비교: 6개 샘플 무회귀,
타겟 샘플 22→4 (-18) 개선.

closes edwardkim#409
Task edwardkim#409 — prev_has_overlay_shape 가드를 Control::Picture (non-TAC) +
TopAndBottom/vert=Para 케이스로 확장하여 차트 다음 문단/표가
차트 높이만큼 추가 점프하는 결함 해결.

LAYOUT_OVERFLOW: 19 -> 1 (21페이지), 22 -> 4 (전체 문서).
1023 lib + 6 svg_snapshot + 통합 테스트 100% 통과.
6개 다른 샘플 무회귀.
prev_has_overlay_shape 가드를 Control::Picture (non-TAC) +
TopAndBottom/vert=Para 케이스로 확장.

LAYOUT_OVERFLOW: 22 -> 4, 21페이지 19 -> 1.
1023 lib + 6 svg_snapshot + 통합 테스트 100% 통과.
이슈 edwardkim#409 재오픈. v1 (layout 측 수정) 후 잔여:
pi=191 헤딩 + pi=192 (10x5 표) 가 21페이지에 묶여
22페이지 SVG 에서 누락되는 결함.

근본 원인: typeset.rs::typeset_section 의 controls 루프가
비-TAC Picture/Shape 의 높이를 current_height 에 누적하지
않아 chart (419.6px) 가 페이지네이션에 미반영. Pagination
추정 used=803.3px (실제 layout y=1275.9px) → 모두 21페이지
packing.

Stage 4: 컨트롤 루프 분기 확장 (TopAndBottom + vert=Para).
Stage 5: 회귀 검증 + 통합 최종 보고서.
typeset.rs::typeset_section 의 controls 루프에 비-TAC +
TopAndBottom + vert=Para 인 Picture/Shape 의 height + margin.bottom
을 current_height 에 누적하는 분기 추가. layout 의
calc_shape_bottom_y 와 동일한 산식.

22페이지에 (4) 헤딩 + 10x5 표 + 연령대별 차트 + 2x1 표가
PDF 와 동일하게 정상 출력. chart 관련 LAYOUT_OVERFLOW 전건
해소 (대상 샘플 4 -> 1, 잔여 1건은 본 변경과 무관한 기존 결함).
1023 lib + 6 svg_snapshot 통과, 6개 다른 샘플 무회귀.
전체 cargo test --release 11개 스위트 100% 통과 (실패 0).
6개 다른 샘플 LAYOUT_OVERFLOW 무회귀.
타겟 샘플 22 -> 1 (chart 관련 전건 해소).

PR 초안을 v1 (layout) + v2 (pagination) 통합본으로 갱신.

closes edwardkim#409
typeset.rs::typeset_section controls 루프에 비-TAC +
TopAndBottom + vert=Para Picture/Shape 의 height + margin.bottom
을 current_height 에 누적. 22페이지 (4) 헤딩 + 10x5 표 정상 표시.

LAYOUT_OVERFLOW (대상 샘플): v1 4 -> v2 1 (chart 관련 전건 해소).
1023 lib + 6 svg_snapshot + 9개 통합 = 11개 스위트 100% 통과.
6개 다른 샘플 무회귀.
typeset.rs::typeset_section 의 비-TAC TopAndBottom + vert=Para
Picture/Shape 높이를 current_height 에 누적. 22페이지에 (4) 헤딩
+ 10x5 표 정상 출력 (PDF 일치).

대상 샘플 LAYOUT_OVERFLOW 22 -> 4 (v1) -> 1 (v2).
11개 테스트 스위트 100% 통과, 6개 다른 샘플 무회귀.

closes edwardkim#409
23페이지 차트(pi=208, TAC Picture, lh=23700 HU)가 SVG에서 24페이지로
밀리는 결함. 차트 시작 y=721.37 (vpos 1460593 -> +626.87px) 가
본문 안이지만 끝 1037.37 이 본문 1028 보다 9.37px 초과.

PDF/HWP는 atomic (분할 불가 단일-line TAC) 항목에 대해
top-fit 시멘틱 사용 — 시작점이 본문 안이면 현재 페이지 배치
하고 하단 일부는 하단 여백(15mm)으로 흘림 허용. 우리는
strict bottom-fit 으로 판정 -> split -> 1-line 못 쪼갬 ->
next page.

Stage 6: typeset_paragraph 의 fit 분기에 atomic TAC top-fit 추가
Stage 7: 회귀 검증 + 통합 최종 보고서 v3
typeset_paragraph 의 fit 분기에 atomic TAC top-fit 추가.
단일 라인 + TAC Picture/Shape 항목은 시작점이 본문 안이고
끝이 60px (약 1.6cm) 이내 초과면 현재 페이지에 배치.
HWP 의 atomic 항목 top-fit 시멘틱 (시작점이 본문 안이면
배치하고 하단 일부는 하단 여백으로 흘림 허용) 을 구현.

23페이지 차트(pi=208, lh=316px) 가 PDF 와 동일하게 표 아래에
정상 배치. 24페이지는 차트가 빠지고 정상 후속 콘텐츠로 시작.

대상 샘플 LAYOUT_OVERFLOW 동일 1건 유지 (chart 정상 배치
이므로 overflow 미발생). 6개 다른 샘플 무회귀.
1023 lib + 6 svg_snapshot 통과.
cargo test --release 11개 스위트 100% 통과 (실패 0).
6개 다른 샘플 무회귀.
타겟 샘플 LAYOUT_OVERFLOW: 22 -> 4 -> 1 -> 1 (chart 정상 배치).

21~24페이지 PDF 대조 전건 일치:
- 21: 2x1 표 차트 직하
- 22: (4) 헤딩 + 10x5 표
- 23: 막대 차트 하단
- 24: 2x1 표 -> (6) 헤딩 -> 파이차트

closes edwardkim#409
typeset.rs::typeset_paragraph 의 fit 분기에 atomic TAC top-fit
추가. 단일 라인 + TAC Picture/Shape 항목은 시작점이 본문 안이고
끝이 60px 이내 초과면 현재 페이지에 배치 (HWP 시멘틱).

23페이지 차트(pi=208) 가 표 아래 정상 배치, 24페이지가 2x1 표
-> (6) 헤딩 -> 파이차트로 정상 시작 (PDF 일치).

11개 테스트 스위트 100% 통과, 6개 다른 샘플 무회귀.
23페이지 차트 정상 배치 + 24페이지 정상 콘텐츠 시작 (PDF 일치).
21~24 페이지 PDF 대조 전건 일치. closes edwardkim#409
- 수행 계획서, 구현 계획서, Stage 1 보고서 작성
- layout.rs:1422 분기에 RHWP_VPOS_DEBUG 진단 로그 추가 (env-gated)
- 6개 샘플 page/lazy × applied/skip 분포 측정
- page_false 케이스를 4개 카테고리로 분류:
  A) 다단 우측 단 base≈7000 (target, 28건) ← fix 대상
  B) vpos reset base≥60000 (합법 스킵, 20건) ← fix 후도 SKIP 유지
  C) base=0 미세 drift (17건) ← 영향 없음
  D) lazy_false 미세 drift ← 영향 없음

closes part of edwardkim#412 (Stage 1)
…irst_vpos 사용

핵심 변경 (layout.rs):
1. col_anchor_y = body_wide_reserved 푸시 직후 y_offset 캡처
   - 첫 PageItem 의 실제 렌더링 y = vpos_page_base 좌표에 해당
   - col_area.y 는 일반적으로 vpos=base 와 일치하지 않아 부정확
2. vpos_end 결정에 curr_first_vpos 우선 사용
   - HWP 가 spacing_after 를 다음 paragraph first vpos 에 인코딩하므로
     prev.vpos+lh+ls 보다 정확
   - vpos reset 시에는 prev 기반 fallback
3. page_path / lazy_path 분리:
   - page_path: col_anchor_y + (vpos_end - base) * scale
   - lazy_path: col_area.y + (vpos_end - base) * scale (기존 유지)

검증 결과 (exam_eng.hwp):
- p1 우측 단 item 7 ①~⑤ 22.55 px 균일 (원래 보고된 버그 해결)
- p1 좌측 단 item 1 ①~⑤ 21.89 px 균일
- p2 우측 단 item 20 ①~⑤ 19.92 px 균일
- p2 우측 단 item 18 ①→② 잔존 (overlay shape bypass — 별도 task)

회귀:
- cargo test 6/6 통과
- golden snapshot: aift-page3 약 3.68 px shift (single ls 적용) — 의도된 개선,
  issue edwardkim#147 본래 검증 목적과 무관해 골든 갱신
- issue-157: anchor 도입 후 자동 정상화

closes part of edwardkim#412 (Stage 2)
검증 대상 (7개 샘플 합계 268페이지):
- exam_eng (8p), exam_kor (24p), exam_math (20p)
- k-water-rfp (28p), 2025년 기부·답례품 (30p)
- aift (78p), kps-ai (80p)

검증 결과:
- cargo test 6/6 통과
- 단단 문서: 영향 없음
- 다단 문서: page_path 보정 정상화 (exam_kor 12건 false→true)
- 시각 회귀 미관찰 (samples 1페이지 시각 비교 7건)

잔존 이슈:
- exam_eng p2 item 18 ①→② (overlay shape bypass)
  → 별도 task 로 추적 권고

closes part of edwardkim#412 (Stage 3)
planet6897 and others added 10 commits April 28, 2026 15:52
- mydocs/report/task_m100_412_report.md 작성
- mydocs/orders/20260428.md: edwardkim#412 항목 완료 갱신

closes edwardkim#412
원인: HY신명조 폰트 메트릭이 ASCII 하이픈을 0.83 em (12.747 px @ 15.31)
으로 저장. 29개 dash 시퀀스가 자연 폭 단계에서 153 px 초과, Justify Branch A
의 공백 압축(min_ews=-2.55px×3) 으로는 회수 불가하여 'of being…' 우측 잘림.

이슈 본문의 'Branch B 가 dash 시퀀스를 단어로 인식해 spread 가산' 가설은
기각 (interior_spaces=3 으로 Branch A 발동 확인).

Stage 2 설계: 3 개 이상 연속 dash leader 시퀀스에만 좁은 advance
(font_size × 0.3) 적용하여 1017 개 정상 dash 회귀 회피.

산출물:
- mydocs/plans/task_m100_352.md (수행계획서)
- mydocs/plans/task_m100_352_impl.md (구현계획서)
- mydocs/troubleshootings/issue_352_root_cause.md (원인 보고서)
- mydocs/working/task_m100_352_stage1.md (단계별 보고서)
3 개 이상 연속 dash 시퀀스를 leader 로 식별하여 좁은 advance(font_size * 0.3)
강제 적용. HY신명조 ASCII '-' 메트릭(0.83 em) 부풀림으로 인한 빈칸 라인
overflow 해소.

text_measurement.rs:
- is_dash_leader_run(chars, i) 헬퍼 추가
- estimate_text_width / compute_char_positions / estimate_text_width_unrounded
  세 char_width 클로저에 적용

Q32 블랭크 라인 측정:
  dash advance: 12.11 → 4.36 px
  29 dash 시퀀스 폭: 351 → 126 px (PDF ~135 px 와 근접)
  'of being' 잘림 해소

자연 텍스트의 단발 dash(stimulus-driven, 32.- 등) 는 ≥3 조건 미충족으로
영향 없음. 1023 단위 테스트 전수 통과.
3 개 이상 연속 ASCII 하이픈 시퀀스의 dash 글리프 출력을 스킵하고,
underline 이 없는 경우에 한해 단일 가로선으로 통합 렌더한다.
가운데점(is_middle_dot) 패턴과 동일한 폰트 비의존 시각 처리.

svg.rs / web_canvas.rs:
- cluster pre-pass 로 dash_run_groups 계산
- shadow + main loop 에서 dash leader 클러스터 스킵
- underline 이 설정된 run 은 dash leader 라인 생략 (이중선 방지)

text_measurement.rs:
- dash leader 좁은 폭 0.3 em → 0.32 em 미세 조정 (PDF ~135 px 일치)

Q32 블랭크 라인 측정 (post Stage 3):
  dash 글리프 수: 29 → 0
  가로선 수: 2 → 1 (underline 만)
  underline 폭: 126.5 → 134.94 px (PDF 목표 ~135 px 일치)

작업지시자 피드백 반영:
  '왜 2줄로 그려지나?' → underline 있을 때 dash leader 라인 생략
  '폭이 조금 짧음' → 0.3 em → 0.32 em

전 테스트 1023 + 통합 테스트 통과. 회귀 없음.
Q32 dash 시퀀스 부풀림 수정 완료:
- dash advance 12.11 → 4.65 px
- 빈칸 폭 351 → 134.94 px (PDF ~135 일치)
- 'of being' 우측 잘림 해소
- 글리프 0 + 단일 underline 으로 시각 정리

closes edwardkim#352
작업지시자 피드백 '폭이 조금 짧음 / 1.5 정도' 에 따라 PDF 를 직접
pdftotext -bbox-layout 으로 실측한 결과 dash 시퀀스 ~218 px (이슈 본문의
135 px 추정은 잘못). 0.32 em (134.94 px) 에서 0.5 em (210.85 px) 으로
조정. PDF 218 px 의 96.7%, 사용자 1.5x estimate 와도 부합. 한컴이 다른
ASCII 구두점에 적용하는 반각 강제 정책과 일관.

text_measurement.rs:
  estimate_text_width / compute_char_positions / estimate_text_width_unrounded
  세 char_width 클로저에 font_size * 0.5 적용

전 테스트 1023+ 통과.
Stage 2~3 의 좁은 dash advance 부작용 해결: dash leader 라인의 슬랙이
단어 공백에 균등 분배되어 단어 사이가 2~3 배 팽창하는 문제.

PDF 직접 실측 (pdftotext -bbox-layout) 결과 PDF 는 dash 를 path/graphic
으로 그리며 라인 슬랙에 따라 가변 폭으로 elastic 하게 채움 (Q32 0.49 em,
Q34 0.30 em — dash 수와 슬랙에 따라 다름). dash 단어가 PDF 텍스트 추출에
나오지 않음이 증거.

변경:
- TextStyle 에 extra_dash_advance 필드 추가 (기본 0.0)
- text_measurement.rs 세 char_width 클로저에 dash leader 만 좁은 base 0.3 em
  + extra_dash_advance 추가
- paragraph_layout.rs Justify 분기에 count_dash_leaders 헬퍼 추가
- 라인에 ≥3 dash leader + 양수 슬랙 시 슬랙을 dash 에 흡수 (공백 자연 폭)
- 음수 슬랙 또는 leader 없으면 기존 Branch A 동작 (공백 압축/분배)

p5 Q32 (s0p221 L6) 측정:
  leader_dashes=29, slack=78.21 px, per_dash_extra=2.70 px
  dash advance 12.11 → 7.06 px, 폭 351 → 204.7 px (PDF 218 의 94%)

p6 Q33 단어 공백 회복:
  Baseline 83.6 px (압축) → Stage 3 125.7 px (팽창) → Stage 5 95.6 px (자연)

전 테스트 1023+ 통과.
Stage 2/5 의 leader-aware advance 와 elastic extra_dash_advance 가
EmbeddedTextMeasurer (Native) 와 estimate_text_width_unrounded 에만
적용되어 있어 WASM 환경에서는 Q32 이후 라인의 단어 공백 팽창이 그대로
재현되던 문제 해결.

WasmTextMeasurer 의 두 char_width 클로저 (estimate_text_width line 577,
compute_char_positions line 679) 에 동일 패턴 추가:
- is_dash_leader_run 검사 → font_size * 0.3 좁은 base 클램프
- is_leader 시 style.extra_dash_advance 가산

paragraph_layout.rs (Justify 분기) 와 web_canvas.rs (시각 렌더) 는 이미
양 경로 공유라 변경 불필요.

검증:
  cargo build --release  → Native PASS
  cargo check --release --target wasm32-unknown-unknown --lib  → PASS
  cargo test --release   → 1023 + 통합 전수 통과
@edwardkim edwardkim added this to the v1.0.0 milestone Apr 28, 2026
@edwardkim edwardkim added the bug Something isn't working label Apr 28, 2026
edwardkim added a commit that referenced this pull request Apr 28, 2026
- PR #415 (Task #352 dash leader Justify, @planet6897) 옵션 A 처리 완료
- 본 PR 의 40 commits 중 Task #352 핵심 7 commits 만 분리 cherry-pick
- 다른 OPEN PR (#401 등) 의 변경 누적 정황 + synam-001 회귀 회피
- 작업지시자 시각 판정 통과 (exam_eng Q32 dash 시퀀스)

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

완료. `devel` 에 Task #352 핵심 7 commits 만 분리 cherry-pick 으로 머지됨 (commit `8daf988`).

이슈 #352 자동 close 됩니다 (PR 본문 `closes #352` 명시).

머지 commit (작성자 attribution 보존)

  • `c0a5592` (cherry-pick) — Stage 1: 원인 확정 (HY신명조 dash 메트릭 853/1024 em)
  • `61db5d6` (cherry-pick) — Stage 2: dash advance 자연 폭 보정 (leader-aware)
  • `5b473f1` (cherry-pick) — Stage 3: dash run 시각 라인 통합 (underline 일치)
  • `a515cd9` (cherry-pick) — Stage 4: 최종 결과 보고서 + orders 갱신
  • `e77beae` (cherry-pick) — 폭 보정 0.32 em → 0.5 em (PDF 실측)
  • `363b7bb` (cherry-pick) — Stage 5: dash leader elastic Justify 분배 (PDF 모방)
  • `368849b` (cherry-pick) — WASM 측정 경로 dash leader 패치

분리 cherry-pick 정황

본 PR 의 40 commits 가 다른 OPEN PR 들 (#401, #406, #408, #410, #414) 의 변경분 누적 형태였습니다. 본 task #352 와 직접 관련된 7 commits 만 분리하여 cherry-pick 한 이유:

확인 결과: cherry-pick 후 `samples/synam-001.hwp` 5 페이지 회귀 정황 발생 안 함 (rows=0..5 정상 유지, 35 페이지 유지).

검증

평가

5 단계 진단 (폰트 메트릭 → Justify 분배 → 시각 라인 통합 → PDF 측정 보정 → elastic 분배) + WASM 경로 동기화 모두 양호합니다. Q32 dash advance 12.11 → 7.06 px (PDF ~7.4 px 근접), 29 dash 폭 351 → 204.7 px (PDF ~218 px) 의 정량 결과도 정확합니다.

특히 Stage 3 의 공백 팽창 사용자 컴플레인을 Stage 5 의 elastic 분배로 정정하신 점 — Justify Branch 7 분기에 `extra_dash_advance` 일관 적용 — 이 깊이 있는 정정이었습니다.

다른 OPEN PR 안내

본 PR 에 누적된 다른 task 들의 PR 흐름:

향후 다른 PR 작업 시 본 PR 처럼 누적되지 않도록, devel 에서 직접 분기하는 것이 검토 / 충돌 측면에서 깔끔합니다.

좋은 기여 감사합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants