fix: 수식 렌더링 개선 — TAC 높이 반영 + 한글 이탤릭 제거 (#174, #175)#388
Conversation
두 가지 수식 렌더링 문제를 수정합니다: 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
There was a problem hiding this comment.
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.
| // 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, | ||
| )); |
There was a problem hiding this comment.
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).
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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::Paren → LayoutKind::Row structure explicitly so the test fails when the structure is unexpected.
| 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); |
| // 한글 텍스트는 이탤릭 보정 없이 폭 산출 | ||
| 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); |
There was a problem hiding this comment.
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.
| // 한글 텍스트는 이탤릭 보정 없이 폭 산출 | |
| 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 | |
| ); |
| // 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); |
There was a problem hiding this comment.
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.
| // 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) | ||
| }; |
There was a problem hiding this comment.
The new equation height/baseline adjustment logic (hwp_eq_h → eq_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.
|
PR 검토했습니다. 코드 변경 (수식 TAC 높이 HWP 권위값 사용 + 한글 CJK 이탤릭 제거) 이 명확하고 CI 도 통과 상태입니다. 다만 PR #387 와 동일하게 본 PR 의 base 가 `main` 으로 설정 되어 있어 main 브랜치 보호 정책으로 BLOCKED 상태입니다. rhwp 프로젝트의 머지 워크플로우는 다음과 같습니다: ``` 따라서 직접 `main` 으로의 PR 은 받지 않습니다. 본 PR 은 close 하시고, `devel` 브랜치 기반으로 다시 PR 을 요청해 주시면 정상 절차로 검토 가능합니다. 재PR 절차:
devel 의 변경 (PR #366 / #371 / #373 / #385) 과는 다른 함수/모듈 영역이므로 충돌 가능성은 낮아 보입니다. PR #387 (밝기/대비) 와 본 PR (수식 렌더링) 두 건 모두 같은 정황이므로 한 번에 정리하시면 편하실 것 같습니다. 수고하셨습니다. 상세 검토: `mydocs/pr/pr_388_review.md` |
- 이슈 #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>
|
검토 감사합니다. PR #387 과 동일하게 main 타겟으로 올린 점 죄송합니다. 두 건 모두 devel 기반으로 재작업하여 새 PR 을 올리겠습니다. 본 PR 은 close 합니다. |
배경
수식 렌더링에서 두 가지 문제가 있었습니다:
변경 사항
수식 TAC 높이 (#174)
eq.common.height)를 우선 사용scale(x, y)변환 적용 — 기존에는 x축만 스케일링하고 y는1로 고정한글 수식 텍스트 (#175)
font-style="italic"제거x,n)만 이탤릭, 한글 설명 텍스트("홀수인 경우")는 정체(upright)테스트
cargo test: 1010 통과 (신규 2건 포함)cargo clippy -- -D warnings: 경고 0건Addresses #174, #175