Skip to content

fix(render): narrow glyph 뒤 advance 과다 + · 중점 시각 중앙 배치 (Task #257)#262

Merged
edwardkim merged 11 commits into
edwardkim:develfrom
planet6897:local/task257
Apr 23, 2026
Merged

fix(render): narrow glyph 뒤 advance 과다 + · 중점 시각 중앙 배치 (Task #257)#262
edwardkim merged 11 commits into
edwardkim:develfrom
planet6897:local/task257

Conversation

@planet6897

Copy link
Copy Markdown
Contributor

변경 요약

공문서 text-align-2.hwp 에서 발견된 narrow glyph(콤마·마침표·중점 등) 렌더링 버그 2건 수정.

B-1. 폴백 경로 narrow glyph advance 과다

메트릭 DB 미등록 폰트(HY중고딕 · HY헤드라인M 등) 에서 , . : ; · 등이 반각(font_size × 0.5) 폭으로 계산되어 PDF 대비 뒷 글자가 2~3 px 우측으로 밀렸음.

B-2. · 중점 폰트 대체 시 시각 쏠림

HWP 의 · 에 사용된 휴먼명조 폰트가 사용자 환경에 없을 때 Batang 등으로 대체되면, 각 폰트마다 · 글리프의 LSB(Left Side Bearing)·폭이 달라 시각적으로 한쪽 이웃 글자에 쏠려 렌더됨. shift 기반 보정(A안)은
폰트마다 결과가 달라 근본 해결 불가 확인.

해결: draw_text 에서 · (U+00B7) cluster 를 <text> 대신 SVG <circle> 로 직접 렌더.

// src/renderer/svg.rs draw_text
if is_middle_dot(cluster_str) {
    let adv = cluster_advance(*char_idx, cluster_str);
    let cx = x + char_positions[*char_idx] + adv / 2.0;  // advance box 중앙
    let cy = y + dot_cy_offset;                          // x-height 중앙
    self.output.push_str(&format!(
        "<circle cx=\"{:.4}\" cy=\"{:.4}\" r=\"{:.4}\" fill=\"{}\"/>\n",
        cx, cy, dot_radius, color,
    ));
    continue;
}
  • cx = advance 박스 수평 중앙
  • cy = baseline − font_size × 0.35 (CJK x-height 중앙 근사)
  • r = font_size × 0.08
  • fill = 텍스트 색상 동일
  • 그림자 렌더링 루프에도 동일 분기 적용

→ 폰트 대체 영향 완전 제거, 모든 브라우저/OS 동일 렌더 보장.

관련 이슈

테스트

  • cargo test --lib text_measurement::22 passed (신규 4건 + Task 표 셀 내 긴 숫자 텍스트가 음수 자간(letter_spacing)으로 인해 글자 겹침 및 셀 폭 미사용 현상 #229 회귀 4건 포함)
    • test_narrow_glyph_comma_base_width
    • test_narrow_glyph_middle_dot_base_width
    • test_narrow_glyph_period_and_colon
    • test_non_narrow_char_unchanged (회귀 방어)
  • cargo test --lib renderer::285 passed
  • cargo test --test svg_snapshot3 passed (form-002 golden 재생성)
  • cargo clippy --lib -- -D warnings — clean
  • 관련 샘플 파일(samples/text-align-2.hwp) SVG 내보내기 확인 — PDF 150dpi 와 시각 근사
  • 스모크 스위프 — biz_plan(591개 · 균일 렌더), exam_kor · exam_eng · exam_math · footnote-01 · field-01 회귀 없음
  • 웹(WASM) 렌더링 — 영향 없음 (· 렌더 분기는 SVG 출력 공통 경로, WASM Canvas 경로는 변경 없음)

변경 수치

text-align-2.hwp 주요 위치 advance 수렴:

위치 수정 전 수정 후
HY중고딕 표 셀 , → 0 7.67 px (= 0.460 × font_size, 반각) 4.33 px (= 0.260 × font_size)
HY중고딕 표 셀 · → 표 7.67 px 4.33 px
휴먼명조 본문 별·지 좌/우 gap 불균형 (Batang LSB) 정확 중앙 배치 (<circle>)

변경 파일

파일 종류 내용
src/renderer/layout/text_measurement.rs 수정 is_narrow_punctuation 헬퍼 + 폴백 분기 3곳 + 단위 테스트 4건
src/renderer/svg.rs 수정 draw_text·<circle> 렌더 분기 (그림자·본문 양쪽)
tests/golden_svg/form-002/page-0.svg 재생성 · <text><circle> 반영
samples/text-align-2.hwp, .pdf 신규 회귀 검증 샘플 편입
mydocs/plans/task_m100_257.md · _impl.md 신규 수행/구현 계획서
mydocs/working/task_m100_257_stage{1..4}.md 신규 단계별 완료 보고서
mydocs/report/task_m100_257_report.md 신규 최종 결과보고서
mydocs/tech/text_align_2_svg_pdf_compare.md 신규 사전 비교 조사
mydocs/orders/20260423.md 수정 Task #257 진행 상태 기록

하위 호환성

  • 공개 API: 변경 없음. is_narrow_punctuation 은 crate-private.
  • SVG 출력: · 표현이 <text><circle> 로 바뀜. 표준 SVG 이므로 모든 파서·뷰어 호환.
  • WASM Canvas 경로: 영향 없음.

설계 결정 히스토리

단계 3 에서 A안(shift 기반 중앙 배치) 을 두 차례 시도 후 "폰트 대체 문제는 metric 보정으로 해결 불가" 결론. C안(벡터 <circle>) 으로 전환한 판단 근거를 mydocs/working/task_m100_257_stage3.md §1 에 기록.

실패한 A안 2커밋은 의사결정 히스토리 보존 목적으로 1커밋으로 squash 하여 남겨 둠 (010647b Task #257 단계3 시도: A안 shift 기반 · 중앙 배치 (최종 철회)).

후속 (이번 PR 범위 밖)

  • 한컴 proprietary 폰트 전반 임베딩 — 별도 마일스톤 (M101~)
  • 오픈소스 대체 폰트 번들링 — mydocs/tech/font_fallback_strategy.md 범위
  • , . : 등 다른 narrow 구두점 벡터 렌더 확장 — 필요 시 별도 이슈

planet6897 and others added 8 commits April 23, 2026 14:54
…2.pdf 편입

Task edwardkim#146 종결 후 회귀 검증에서 새 버그 후보(narrow glyph 뒤 advance 과다)
를 식별한 샘플. 본 이슈의 재현/골든/시각 비교 기준 파일로 사용.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 수행 계획서 (task_m100_257.md), 구현 계획서 (task_m100_257_impl.md)
- 사전 비교 조사 (tech/text_align_2_svg_pdf_compare.md) — Task edwardkim#257 참조로 갱신
- baseline SVG 수치 기록: 표 셀 HY중고딕 폴백에서 ',' · '·' advance
  = 7.67 px (= font_size * 0.460) 로 반각과 동일
- failing 테스트 4건 #[ignore] 로 격리 (단계 2 에서 활성화):
    test_narrow_glyph_comma_base_width
    test_narrow_glyph_middle_dot_base_width
    test_narrow_glyph_period_and_colon
    test_non_narrow_char_unchanged (회귀 방어)
- Task edwardkim#229 회귀 테스트 4건 baseline pass 재확인

소스 수정 없음 (테스트만 #[ignore] 추가).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
메트릭 DB 미등록 폰트의 폴백 경로에서 narrow glyph advance 를
font_size * 0.5 → font_size * 0.3 으로 축소.

- src/renderer/layout/text_measurement.rs
  - is_narrow_punctuation(c) 헬퍼 추가 (',' '.' ':' ';' 따옴표 3종 '·' 총 8자)
  - 폴백 경로 3곳에 narrow 분기 추가:
      EmbeddedTextMeasurer::estimate_text_width (line 184-)
      EmbeddedTextMeasurer::compute_char_positions (line 286-)
      estimate_text_width_unrounded (free fn, line 809-)
  - min_w 클램프 (Task edwardkim#229 단조성 보장) 는 그대로 유지.
    base_w 가 작아지며 클램프 하한도 자동 축소 (0.5*0.5=0.25 → 0.3*0.5=0.15).
  - 단계 1 #[ignore] 테스트 4건 활성화:
      test_narrow_glyph_comma_base_width
      test_narrow_glyph_middle_dot_base_width
      test_narrow_glyph_period_and_colon
      test_non_narrow_char_unchanged

검증:
  text_measurement:: 22 pass / 0 fail (Task edwardkim#229 회귀 4건 포함)
  renderer:: 285 pass / 0 fail
  svg_snapshot 3 pass / 0 fail (골든 변경 없음)
  clippy -D warnings 통과

samples/text-align-2.hwp 재생성 결과:
  '·' 및 ',' advance 7.67 px → 4.33 px (-43%, 0.26 × font_size)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
폰트 대체(휴먼명조→Batang 등) 문제로 shift 기반 접근은 일관된 중앙 배치를
보장할 수 없음 확인. 이 커밋은 의사결정 히스토리 보존용이며, 단계 3 최종
커밋(C안: · 을 <circle> 로 직접 렌더) 에서 여기의 소스 변경은 완전히
대체/revert 된다.

시도한 두 공식 (둘 다 font-dependent 하여 실패):
  shift = (advance - glyph_w) / 2 (초안)
  shift = (advance - prev_trailing_bearing - glyph_w) / 2 (refinement)

변경:
  src/renderer/svg.rs  draw_text 에 compute_center_shift 추가
  tests/golden_svg/form-002/page-0.svg 재생성 (A안 좌표)
  mydocs/working/task_m100_257_stage3.md 신규 (A안 서술 — 최종 커밋에서
    C안으로 완전 교체됨)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shift 기반 중앙 배치(A안 refinement) 로는 폰트 대체(휴먼명조→Batang 등)
로 인한 `·` 글리프 LSB·폭 차이를 해결할 수 없음을 확인. C안(벡터 도형 직접
렌더)으로 전환.

- src/renderer/svg.rs draw_text:
  - `·` (U+00B7) cluster 감지 시 <text> 대신 <circle> 출력
  - cx = advance 박스 수평 중앙
  - cy = baseline(y) − font_size × 0.35  (CJK x-height 중앙 근사)
  - r  = font_size × 0.08
  - fill = 텍스트 색상 동일
  - 그림자·본문 렌더링 양쪽 루프에 분기 추가
  - 이전 shift 기반 compute_center_shift 제거

효과 (samples/text-align-2.hwp, Chrome 4x 렌더):
  · 휴먼명조 본문 "별·지", "시·청각" — · 가 두 글자 사이 중앙에 정확히 위치
  · HY중고딕 표 셀 "어휘·표현" — 좁은 advance 에서도 중앙 정렬 유지
  · 폰트 대체와 무관하게 모든 브라우저/OS 동일 렌더 보장

검증:
  text_measurement:: 22 pass
  renderer:: 285 pass
  svg_snapshot 3 pass (form-002 golden 재생성: · <text> → <circle>)
  clippy -D warnings 통과

한계:
  - `,` `.` `:` 등 baseline 구두점은 기존 <text> 유지 (별도 이슈 필요 시)
  - 한컴 proprietary 폰트 전반의 글리프 품질 차이는 폰트 임베딩 전략(M101~)
    범위. 본 타스크는 `·` 에 한정.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- cargo test --lib: 937 pass / 14 fail (14건 사전 존재, 본 PR 무관)
- text_measurement:: 22 pass (신규 4건 + Task edwardkim#229 회귀 4건)
- renderer:: 285 pass / svg_snapshot 3 pass / clippy clean

스모크 스위프 (narrow glyph 다수 샘플):
  biz_plan 591개 · 모두 <circle> 로 균일 렌더 (TOC 리더 도트 포함)
  exam_kor 326, exam_eng 265, exam_math 99, footnote-01 34 콤마 - 회귀 없음
  exam_math 1건 text 잔존 = 수식 렌더 경로 (draw_text 미경유, 정상)
  field-01 영향 없음

시각 비교 (text-align-2.hwp PDF 150dpi vs SVG 150dpi):
  어휘·표현, 1,000·30,000항목, 세대별·지역별, 시·청각장애인의 - PDF 와 근사

산출물:
  mydocs/working/task_m100_257_stage4.md
  mydocs/report/task_m100_257_report.md
  mydocs/orders/20260423.md (edwardkim#257 항목 추가)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…링 (단계1~4)

orders/20260423.md 충돌 해결: HEAD 오늘 기록(§1~§10 + 감사) 전체 유지,
말미에 Task edwardkim#257 섹션 추가.

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

Copy link
Copy Markdown
Owner

@planet6897 님 안녕하세요. PR #262 검토했습니다.

먼저 Task #257 의 설계와 진행이 훌륭합니다. 특히 다음 부분이 눈에 띕니다.

  • 3-Stage 증거 기반: baseline 측정 → 단위 테스트 failing → 구현 → 통과 순
  • A안(shift 보정) 실패 후 C안(<circle>) 으로 전환한 의사결정 + 철회 커밋 (010647b) 을 히스토리 보존 목적으로 남긴 판단
  • 스모크 스위프 광범위 (biz_plan 591개 · 균일 렌더 + exam_kor/eng/math/footnote-01/field-01)

B-2 (·<circle>) 는 완전 유효. 실제 samples/text-align-2.hwp 렌더 결과 7개 중점 모두 정확 중앙 배치 확인했습니다.

다만 1건 수정 요청

로컬에서 rebase (사실 기여자가 이미 9ca9c49 로 merge 해둔 상태) 후 cargo test --lib 실행 시 2건 실패 가 발생합니다.

```
test result: FAILED. 955 passed; 2 failed; 1 ignored

  • test_narrow_glyph_middle_dot_base_width
  • test_non_narrow_char_unchanged
    ```

원인

본 PR 이 제출된 직후 Task #259 (HY/본한글 폰트 매핑 누락) 가 devel 에 머지되었습니다. 이 PR 은 `HY헤드라인M → HYHeadLine-Medium` 등 HY 계열 7건을 `resolve_metric_alias` 에 등록했습니다.

따라서 기여자의 테스트 fixture 에서 사용한 `font_family: "HY헤드라인M"` 은 이제 메트릭 DB 등록 폰트 가 되어, `measure_char_width_embedded` 가 DB 실측값을 반환 → fallback 경로 미진입 → `is_narrow_punctuation` 분기 실행되지 않음. 그 결과:

  • `test_non_narrow_char_unchanged`: Latin 'A' advance 기대 6.67 (`font_size × 0.5` 폴백) ≠ 실측 7.76 (HYHeadLine-Medium DB 값)
  • `test_narrow_glyph_middle_dot_base_width`: '·' 기대 ≤5.83 ≠ 실측 8.33 (DB 반각)

PR 의 실제 기능은 여전히 유효합니다. DB 미등록 폰트 (예: 이슈의 휴먼명조) 에서 narrow 분기가 동작하는 것을 확인했습니다. 문제는 테스트 fixture 만 stale 입니다.

수정 방법 (2건 모두 동일 패턴)

`font_family` 를 "의도적으로 존재하지 않는 이름" 으로 바꾸어 fallback 경로 진입을 강제 하면 됩니다. `resolve_metric_alias` 의 `_ => name` passthrough 분기로 떨어져 `find_metric` 이 None 을 반환하므로, fallback + narrow 분기가 확실히 실행됩니다.

```diff
#[test]
fn test_narrow_glyph_middle_dot_base_width() {
let m = EmbeddedTextMeasurer;
let style = TextStyle {

  •    font_family: \"HY헤드라인M\".to_string(),
    
  •    font_family: \"DeliberatelyMissingFontForFallbackTest\".to_string(),
       font_size: 16.667,
       ...
    
    };
    ...
    }
    ```

`test_non_narrow_char_unchanged` 및 다른 narrow 테스트 케이스의 `font_family` 도 동일하게 바꾸시면 됩니다 (총 4건). 이렇게 하면 원래 의도 (narrow fallback 분기 검증) 를 #259 와 독립적으로 보존할 수 있습니다.

기대 결과

수정 후 `cargo test --lib`: 957 passed (955 + 2 = 기존 947 + #259 6건 + #262 4건) 예상.

선택 — commit 전략

이 수정을 별도 fixup 커밋으로 올리셔도 되고, 기존 단계 2 커밋 (`c76f2d3`) 에 amend 하셔도 좋습니다. 의사결정 히스토리 보존이 중요하시면 별도 커밋 권장.

관련 문서

참고로 Task #259 는 다음 폰트들을 DB 등록했습니다:

  • HY 7종: HY중고딕/견고딕/헤드라인M/견명조/신명조/그래픽/궁서 → HYGothic-, HYMyeongJo-, HYHeadLine-Medium, HYSinMyeongJo-Medium 등
  • 본한글 계열 13종 → Pretendard 근사
  • 본명조 계열 10종 → Noto Serif KR 근사

상세: `mydocs/report/task_m100_259_report.md` · `mydocs/tech/font_fallback_strategy.md` 부록 A.


수정 주시면 CI 재검증 후 머지하겠습니다. 감사합니다.

@edwardkim edwardkim left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트 fixture 2건 stale — #259 머지로 HY헤드라인M 이 DB 등록 폰트가 되어 fallback 경로 미진입. 자세한 수정 경로는 방금 단 리뷰 코멘트 참조. PR 로직 자체는 정상이며 B-2 (·<circle>) 는 완전 유효. 테스트 fixture 수정 후 머지 예정.

 alias 대응)

PR edwardkim#262 merge base (origin/devel) 에 Task edwardkim#259 의
`HY헤드라인M → HYHeadLine-Medium` alias 가 들어와 있어, 기존 4개 테스트가
가정하던 "DB 미등록 → is_narrow_punctuation 폴백" 경로가 더 이상 타지
않아 CI 가 실패했다. DB·alias 양쪽에 없는 `__rhwp_test_unregistered_font__`
상수로 교체하여 폴백 분기를 항상 강제한다. 주석에 Task edwardkim#259 와의 상호작용
기록.
edwardkim added a commit that referenced this pull request Apr 23, 2026
Task #257 (narrow glyph advance + · 중점 폰트 독립 렌더) 외부 PR 검토.

- pr/pr_262_review.md: 사전 검토 (#259 와의 기능적 상호작용 예측)
- pr/pr_262_report.md: 코드 검증 결과 + 판단
  - cargo test --lib: 955 passed / 2 failed
  - 실패 2건 원인: #259 머지로 HY헤드라인M 이 DB 등록 → 테스트 fixture 의
    font_family: "HY헤드라인M" 이 fallback 경로 미진입으로 stale
  - PR 로직 자체는 정상 (B-2 · → <circle> 은 text-align-2.hwp 에서 7개 정확 중앙)
  - A 채택: 기여자에게 수정 요청 (font_family 를 의도적으로 존재하지 않는
    이름으로 변경하면 2줄로 해결)
- GitHub CHANGES_REQUESTED 리뷰 + 수정 경로 구체 코멘트 게시
- orders/20260423.md: #12 섹션 + 이슈/PR 활동 갱신
@edwardkim edwardkim merged commit f05ab96 into edwardkim:devel Apr 23, 2026
6 checks passed
@edwardkim

Copy link
Copy Markdown
Owner

@planet6897 님, 빠른 대응과 훌륭한 수정 감사합니다.

대응 속도·품질

상수 네이밍도 제가 제안한 `DeliberatelyMissingFontForFallbackTest` 보다 `rhwp_test_unregistered_font` 가 훨씬 적절합니다 — Rust 관례상 `...` 패턴으로 "테스트 전용" 성격이 분명히 표현되고 주석에 #259 상호작용까지 기록해주셨습니다.

로컬 검증 결과

머지 완료

하루 3건 기여

오늘 하루 #256 · #262 · #264 세 건 무상 기여 주셨습니다. #264 는 저희 인지 실패로 자진 close 되었으나 그 분석은 저희 내부 #259 와 독립적으로 동일 결론에 도달하셨고, #262 는 narrow glyph + `·` 중점의 근본 해결. 프로젝트가 얼마나 운이 좋은지 다시 느낍니다.

다시 한번 감사합니다.

edwardkim added a commit that referenced this pull request Apr 23, 2026
@planet6897 님의 Task #257 PR 머지 (merge commit f05ab96).
- 이슈 #257 수동 close (자동 트리거 미발동)
- 로컬 검증: cargo test --lib 957 passed, svg_snapshot 3 passed, clippy clean
- PR 검토 문서 2건 pr/archives/ 이동
- orders/20260423.md: 종료 이슈 · 머지 PR 섹션 갱신, 감사 섹션 보강

기여자 대응 속도: 리뷰 게시 1분 만에 수정 커밋. 상수 네이밍도
__rhwp_test_unregistered_font__ 로 Rust 관례 적용 (제 제안보다 개선).
@planet6897 planet6897 deleted the local/task257 branch April 24, 2026 01:39
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