Skip to content

fix: 수식 렌더링 개선 — TAC 높이 반영 + 한글 이탤릭 제거 (#174, #175)#388

Closed
oksure wants to merge 1 commit into
edwardkim:mainfrom
oksure:contrib/equation-rendering-improvements
Closed

fix: 수식 렌더링 개선 — TAC 높이 반영 + 한글 이탤릭 제거 (#174, #175)#388
oksure wants to merge 1 commit into
edwardkim:mainfrom
oksure:contrib/equation-rendering-improvements

Conversation

@oksure

@oksure oksure commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

배경

수식 렌더링에서 두 가지 문제가 있었습니다:

  1. 수식 TAC 높이가 줄 높이에 미반영 — 큰 수식과 텍스트 겹침 #174: 큰 수식(∑, 분수, lim 등)이 포함된 줄과 인접 줄이 겹침
  2. CASES+EQALIGN 한글 혼합 수식 오버래핑 #175: CASES 수식 내 한글+수학 혼합 텍스트가 이탤릭으로 렌더링됨

변경 사항

수식 TAC 높이 (#174)

  • 인라인 수식의 BoundingBox 높이를 레이아웃 엔진 산출값 대신 HWP 저장 높이(eq.common.height)를 우선 사용
  • SVG 렌더러에서 scale(x, y) 변환 적용 — 기존에는 x축만 스케일링하고 y는 1로 고정
  • HWP 높이와 레이아웃 높이 차이에 비례하여 baseline도 조정

한글 수식 텍스트 (#175)

  • 수식 내 CJK 텍스트(한글 등)에 font-style="italic" 제거
    • 수학 변수명(x, n)만 이탤릭, 한글 설명 텍스트("홀수인 경우")는 정체(upright)
  • 한글 텍스트 폭 산출 시 이탤릭 1.05배 보정 제외
  • CASES 행 겹침 방지 검증 테스트 추가

테스트

  • cargo test: 1010 통과 (신규 2건 포함)
  • cargo clippy -- -D warnings: 경고 0건
  • SVG 스냅샷 테스트 6건 통과 (회귀 없음)

Addresses #174, #175

두 가지 수식 렌더링 문제를 수정합니다:

1. 수식 TAC 높이 미반영 (edwardkim#174)
   - 인라인 수식의 BoundingBox 높이를 레이아웃 엔진 산출값 대신
     HWP 저장 높이(eq.common.height)를 우선 사용
   - SVG 렌더러에서 y축 스케일링 추가 (기존 x축만 적용)
   - 높이 차이에 비례하여 baseline도 조정

2. CASES+EQALIGN 한글 혼합 수식 겹침 (edwardkim#175)
   - 수식 내 한글(CJK) 텍스트에 font-style="italic" 제거
     (수학 변수명만 이탤릭, 한글 설명 텍스트는 정체)
   - 한글 텍스트 폭 산출 시 이탤릭 1.05배 보정 제외
   - CASES 행 겹침 방지 테스트 추가

테스트: cargo test 1010 통과, cargo clippy 경고 0건

Addresses edwardkim#174, edwardkim#175
Copilot AI review requested due to automatic review settings April 27, 2026 11:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR improves equation rendering fidelity in the SVG backend by (1) incorporating the HWP-stored equation height into inline layout to prevent line overlap for tall formulas, and (2) rendering CJK (e.g., Korean) equation text upright (non-italic) to avoid visual/spacing issues in mixed-script CASES expressions.

Changes:

  • Use eq.common.height (HWP stored height) as the primary inline equation height and proportionally adjust the equation baseline placement in paragraph layout.
  • Apply both X and Y scaling when placing equation SVG content into its bounding box (previously only X scaling was applied).
  • Detect CJK text in equation rendering/layout to avoid italic styling and italic width correction; add tests to prevent CASES row overlap regressions.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/renderer/svg.rs Adds Y-axis scaling for equation SVG placement to match bbox height.
src/renderer/layout/paragraph_layout.rs Prefers HWP equation height and scales baseline alignment accordingly to prevent inter-line overlaps.
src/renderer/equation/svg_render.rs Removes italic styling for CJK equation text during SVG generation.
src/renderer/equation/layout.rs Excludes italic width correction for CJK text and adds regression tests for CASES overlap / width behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +46 to 54
// CJK/한글 텍스트는 이탤릭 없이 렌더링 (수학 변수명만 이탤릭)
let has_cjk = text.chars().any(|c| matches!(c,
'\u{3000}'..='\u{9FFF}' | '\u{F900}'..='\u{FAFF}' | '\u{AC00}'..='\u{D7AF}'
));
let style = if has_cjk { "" } else { " font-style=\"italic\"" };
svg.push_str(&format!(
"<text x=\"{:.2}\" y=\"{:.2}\" font-size=\"{:.2}\" fill=\"{}\" font-style=\"italic\"{}>{}</text>\n",
text_x, text_y, fi, color, EQ_FONT_FAMILY, esc,
"<text x=\"{:.2}\" y=\"{:.2}\" font-size=\"{:.2}\" fill=\"{}\"{}{}>{}</text>\n",
text_x, text_y, fi, color, style, EQ_FONT_FAMILY, esc,
));

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

render_box receives italic/bold flags (set by LayoutKind::FontStyle) but LayoutKind::Text currently ignores them and always applies italic for non‑CJK. This makes rm/it/bf style nodes ineffective for plain text. Consider deriving font-style from italic (and omitting it when !italic or when has_cjk), and also applying font-weight when bold is true (similar to LayoutKind::Number).

Copilot uses AI. Check for mistakes.
Comment on lines +1023 to +1034
if let LayoutKind::Paren { body, .. } = &lb.kind {
if let LayoutKind::Row(rows) = &body.kind {
assert!(rows.len() >= 2, "CASES should have at least 2 rows");
let row1 = &rows[0];
let row2 = &rows[1];
let row1_bottom = row1.y + row1.height;
let row2_top = row2.y;
assert!(row2_top >= row1_bottom,
"CASES rows should not overlap: row1 bottom={:.1}, row2 top={:.1}",
row1_bottom, row2_top);
}
}

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

This test can pass without actually validating the expected CASES layout because the nested if let chain has no else/panic branch. If parse_and_layout returns a different structure (or a regression changes it), the overlap assertions are silently skipped. Consider asserting the expected LayoutKind::ParenLayoutKind::Row structure explicitly so the test fails when the structure is unexpected.

Suggested change
if let LayoutKind::Paren { body, .. } = &lb.kind {
if let LayoutKind::Row(rows) = &body.kind {
assert!(rows.len() >= 2, "CASES should have at least 2 rows");
let row1 = &rows[0];
let row2 = &rows[1];
let row1_bottom = row1.y + row1.height;
let row2_top = row2.y;
assert!(row2_top >= row1_bottom,
"CASES rows should not overlap: row1 bottom={:.1}, row2 top={:.1}",
row1_bottom, row2_top);
}
}
let body = match &lb.kind {
LayoutKind::Paren { body, .. } => body,
other => panic!(
"CASES should be wrapped in LayoutKind::Paren, got {:?}",
other
),
};
let rows = match &body.kind {
LayoutKind::Row(rows) => rows,
other => panic!(
"CASES paren body should be LayoutKind::Row, got {:?}",
other
),
};
assert!(rows.len() >= 2, "CASES should have at least 2 rows");
let row1 = &rows[0];
let row2 = &rows[1];
let row1_bottom = row1.y + row1.height;
let row2_top = row2.y;
assert!(row2_top >= row1_bottom,
"CASES rows should not overlap: row1 bottom={:.1}, row2 top={:.1}",
row1_bottom, row2_top);

Copilot uses AI. Check for mistakes.
Comment on lines +1039 to +1045
// 한글 텍스트는 이탤릭 보정 없이 폭 산출
let korean = parse_and_layout("홀수인~경우", 20.0);
let latin = parse_and_layout("abcdef", 20.0);
// 한글 6자(전각 1.0×) > 라틴 6자(~0.55×)
assert!(korean.width > latin.width,
"Korean text width ({:.1}) should be larger than Latin ({:.1})",
korean.width, latin.width);

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

test_korean_text_width_not_italic doesn’t actually verify that the italic width correction was removed for Korean text (it only checks Korean width > Latin width, which would likely be true even when Korean still receives the 1.05 italic multiplier). Consider asserting against an expected numeric relationship (e.g., Korean width equals the non-italic estimate within an epsilon, or that the same Korean string width is ~1/1.05 of the italic-estimated width) so the test specifically guards this regression.

Suggested change
// 한글 텍스트는 이탤릭 보정 없이 폭 산출
let korean = parse_and_layout("홀수인~경우", 20.0);
let latin = parse_and_layout("abcdef", 20.0);
// 한글 6자(전각 1.0×) > 라틴 6자(~0.55×)
assert!(korean.width > latin.width,
"Korean text width ({:.1}) should be larger than Latin ({:.1})",
korean.width, latin.width);
// 한글 텍스트는 전각(1.0×) 기준으로, 이탤릭 보정(1.05×) 없이 폭이 산출되어야 함
let font_size = 20.0;
let korean_text = "가나다";
let korean = parse_and_layout(korean_text, font_size);
let expected_non_italic_width = 3.0 * font_size;
let expected_italic_width = expected_non_italic_width * 1.05;
let epsilon = 0.01;
assert!(
(korean.width - expected_non_italic_width).abs() <= epsilon,
"Korean text width ({:.2}) should match the non-italic estimate ({:.2})",
korean.width,
expected_non_italic_width
);
assert!(
(korean.width - expected_italic_width).abs() > epsilon,
"Korean text width ({:.2}) should not include italic correction ({:.2})",
korean.width,
expected_italic_width
);

Copilot uses AI. Check for mistakes.
Comment on lines +226 to +230
// CJK/한글 텍스트는 이탤릭이 아니므로 italic 보정 제외
let has_cjk = text.chars().any(|c| matches!(c,
'\u{3000}'..='\u{9FFF}' | '\u{F900}'..='\u{FAFF}' | '\u{AC00}'..='\u{D7AF}'
));
let w = estimate_text_width(text, fs, !has_cjk);

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

The CJK detection logic (the has_cjk Unicode range checks) is now duplicated here and in equation/svg_render.rs. To avoid the two drifting over time (e.g., adding Hangul Jamo / Kana ranges in one place only), consider extracting this into a small shared helper in the equation module (e.g., contains_cjk(text: &str)) and reusing it in both layout and SVG rendering.

Copilot uses AI. Check for mistakes.
Comment on lines +1736 to +1746
// HWP 저장 높이를 우선 사용 (한컴 조판 결과 기준)
let hwp_eq_h = hwpunit_to_px(eq.common.height as i32, self.dpi);
let eq_h = if hwp_eq_h > 0.0 { hwp_eq_h } else { layout_box.height };
// 수식 baseline을 텍스트 baseline에 맞춤
let eq_y = (y + baseline - layout_box.baseline).max(y);
// HWP 높이와 레이아웃 높이가 다르면 baseline도 비례 조정
let eq_y = if hwp_eq_h > 0.0 && layout_box.height > 0.0 {
let scale = hwp_eq_h / layout_box.height;
(y + baseline - layout_box.baseline * scale).max(y)
} else {
(y + baseline - layout_box.baseline).max(y)
};

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

The new equation height/baseline adjustment logic (hwp_eq_heq_h and the scaled-baseline eq_y) is duplicated in two separate inline-equation code paths. Consider extracting this into a small helper (taking layout_box, eq.common.height, y, baseline, dpi) so future fixes don’t accidentally update only one path.

Copilot uses AI. Check for mistakes.
@edwardkim

Copy link
Copy Markdown
Owner

PR 검토했습니다.

코드 변경 (수식 TAC 높이 HWP 권위값 사용 + 한글 CJK 이탤릭 제거) 이 명확하고 CI 도 통과 상태입니다. 다만 PR #387 와 동일하게 본 PR 의 base 가 `main` 으로 설정 되어 있어 main 브랜치 보호 정책으로 BLOCKED 상태입니다.

rhwp 프로젝트의 머지 워크플로우는 다음과 같습니다:

```
컨트리뷰터 PR → devel (개발 통합) → 릴리즈 시점에 devel → main + 태그
```

따라서 직접 `main` 으로의 PR 은 받지 않습니다. 본 PR 은 close 하시고, `devel` 브랜치 기반으로 다시 PR 을 요청해 주시면 정상 절차로 검토 가능합니다.

재PR 절차:

  1. 로컬에서 `devel` 최신 fetch
  2. 새 브랜치를 `devel` 위에 생성 (예: `contrib/equation-rendering-improvements-v2`)
  3. 본 PR 의 commit 을 cherry-pick 또는 동일 변경을 새로 commit
  4. base=`devel` 로 PR 생성

devel 의 변경 (PR #366 / #371 / #373 / #385) 과는 다른 함수/모듈 영역이므로 충돌 가능성은 낮아 보입니다.

PR #387 (밝기/대비) 와 본 PR (수식 렌더링) 두 건 모두 같은 정황이므로 한 번에 정리하시면 편하실 것 같습니다.

수고하셨습니다.

상세 검토: `mydocs/pr/pr_388_review.md`

edwardkim added a commit that referenced this pull request Apr 27, 2026
- 이슈 #377: bytes 반환 API 이미 제공 안내 + 위키 가이드 링크 + close
- mydocs/feedback/issue_377_export_api_review.md 추가
- mydocs/orders/20260426.md 갱신
- mydocs/pr/pr_387_review.md, pr_388_review.md (devel 기반 재PR 안내 후 작성자 close 대기)

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

oksure commented Apr 27, 2026

Copy link
Copy Markdown
Contributor Author

검토 감사합니다. PR #387 과 동일하게 main 타겟으로 올린 점 죄송합니다. 두 건 모두 devel 기반으로 재작업하여 새 PR 을 올리겠습니다. 본 PR 은 close 합니다.

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.

3 participants