fix: 수식 렌더링 개선 — TAC 높이 반영 + 한글 이탤릭 제거 (#174, #175)#396
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
Improves equation rendering to better match HWP composition results by using stored equation height metrics and by preventing unintended italic styling/width adjustments for CJK text in equations.
Changes:
- Apply both X/Y scaling when placing equation SVG fragments to better match stored (HWP) equation bounding boxes.
- Prefer HWP-stored equation height (
eq.common.height) for inline equation layout and proportionally adjust equation baseline placement. - Detect CJK text in equations and avoid applying italic styling and italic width correction; adds regression tests for CASES overlap and CJK width behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/renderer/svg.rs |
Adds Y-axis scaling (in addition to X) when embedding equation SVG content into the page SVG. |
src/renderer/layout/paragraph_layout.rs |
Uses HWP-stored equation height for inline placement and scales baseline alignment when height differs from layout-derived height. |
src/renderer/equation/svg_render.rs |
Skips italic styling for CJK text when rendering equation SVG <text> elements. |
src/renderer/equation/layout.rs |
Skips italic width correction for CJK equation text and adds regression tests for CASES overlap + CJK width. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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.
test_cases_korean_no_overlap currently only checks for overlap inside nested if let matches; if the CASES layout shape changes (e.g., not Paren or body not a Row), the test will still pass without validating the no-overlap invariant. Consider making the expected structure explicit (or returning an error) so the test fails when it can’t reach the overlap assertion.
| 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 laid out as Paren, got {:?}", | |
| other | |
| ), | |
| }; | |
| let rows = match &body.kind { | |
| LayoutKind::Row(rows) => rows, | |
| other => panic!( | |
| "CASES body should be laid out as 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 | |
| ); |
| // 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.
This change removes italic styling for CJK in the SVG equation renderer, but the Canvas equation renderer still forces italic for LayoutKind::Text (see src/renderer/equation/canvas_render.rs:42-47). If Canvas output is user-facing (used by src/renderer/web_canvas.rs), the “CJK not italic” fix will be inconsistent across backends; consider applying the same CJK handling there as well.
| // 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 inline-equation placement logic (HWP height priority + baseline scaling) is duplicated in two separate code paths. To reduce the risk of future divergence, consider extracting this into a small helper/closure that returns (eq_h, eq_y) given (layout_box, eq.common.height, baseline, y).
1. test_cases_korean_no_overlap: if-let silent pass → match + panic!
으로 구조 변경 시 테스트가 명시적으로 실패하도록 개선.
실제 구조가 Row[..., Paren{cases}]임을 확인하고 테스트 수정.
2. canvas_render.rs: CJK 수식 텍스트 이탤릭 제거를 SVG 렌더러와
동일하게 Canvas 렌더러에도 적용 (백엔드 일관성).
|
Copilot 리뷰 피드백 반영했습니다 (dbaa74b):
|
PR #396 (@oksure) cherry-pick 후 메인테이너 후속 정정: - 결함: Canvas 경로 (rhwp-studio 웹 에디터) 의 LayoutKind::Fraction 처리에서 분수선 y = baseline 으로 그려져 분모와 겹치는 정황. SVG 경로 (svg_render.rs) 는 baseline - fs * AXIS_HEIGHT 로 정상 처리됐으나 canvas_render.rs 가 누락. - 정정: line_y 계산을 SVG 와 동일하게 변경 (- fs * super::layout::AXIS_HEIGHT) - 작업지시자가 web 에디터 시각 판정 중 발견 — exam_math.hwp 의 분수 분모와 가로선 겹침 결함 검증: - cargo test --lib: 1031 passed (무회귀) - cargo test --test svg_snapshot: 6/6 - cargo test --test issue_418: 1/1 (Task #418 보존) - cargo clippy --lib -- -D warnings: warning 0건 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #396 의 svg.rs Y축 스케일링 정정 (scale_x, 1) → (scale_x, scale_y) 가 SVG 경로에만 적용되어, Canvas 경로 (rhwp-studio web 에디터) 에서는 수식이 SVG 와 다르게 그려지는 정황. 정정: - src/renderer/web_canvas.rs:397 의 Equation 분기에 scale_x / scale_y 적용 - HWP 저장 영역(bbox) 과 레이아웃 산출 크기(layout_box) 비율로 ctx.scale 호출 - SVG 경로 (svg.rs:328-348) 와 동일한 동작 검증: - cargo build --lib + cargo check --target wasm32-unknown-unknown --lib 통과 - cargo test --lib: 1031 passed (무회귀) - cargo test --test svg_snapshot: 6/6 - cargo test --test issue_418: 1/1 - cargo clippy --lib -- -D warnings: warning 0건 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
증상: - web 에디터에서 lim (극한 함수) 글자가 다른 글자보다 약 1.5배 크게 표시 - SVG 경로는 정상 — Canvas 만 결함 원인: - canvas_render.rs::Limit 의 fi = font_size_from_box(lb, fs) - Limit 의 LayoutBox.height 는 "lim 텍스트 + sub 첨자" 를 포함한 wrapper 높이 - font_size_from_box 가 lb.height 를 반환하므로 base_fs 의 1.5~2 배 → lim 글자 확대 - SVG 경로 (svg_render.rs::Limit) 는 fi = fs 로 정상 정정: - canvas_render.rs::Limit 의 fi = fs 로 SVG 와 일치화 - 다른 텍스트 분기 (Text/Number/Symbol/MathSymbol/Function) 는 LayoutBox 가 본인 텍스트만 포함해서 lb.height ≈ fs 라 font_size_from_box 정상 동작 — 변경 없음 검증: - cargo test --lib: 1031 passed (무회귀) - cargo test --test svg_snapshot: 6/6 - cargo test --test issue_418: 1/1 - cargo clippy --lib -- -D warnings: warning 0건 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
완료. `devel` 에 cherry-pick (작성자 attribution 보존) + 메인테이너 후속 정정으로 머지됨 (commit `0d51736`). 이슈 #174, #175 의 SVG 경로 정정이 적용됐습니다. 머지 commit작성자 attribution 보존:
메인테이너 후속 정정 (Canvas 경로 SVG 일치화):
후속 정정 정황작업지시자가 web 에디터 (Canvas 경로) 와 SVG 경로 시각 비교에서 다음 결함 발견:
각 결함의 원인은 Canvas 경로가 SVG 경로와 일관되지 않은 코드 조각 (`font_size_from_box` 사용, scale 누락 등). 본 PR 후속 정정으로 SVG 경로와 일치화. 검증
참고
좋은 기여 감사합니다. |
본질: parse_command 의 OVER/ATOP 폐기로 parse_cases / parse_pile / parse_eqalign / parse_matrix 의 row-collecting 루프에서 분수가 분실됨. PR edwardkim#396 (edwardkim#175 후속) 회귀 테스트가 다루지 못한 토폴로지. 발견 경로: all-in-one-parser fixture 1:1 정합화 전략의 A' 진단 → 미적분03.hwp p5 의 g(x)= {cases{{1} over {2} x ^{2} ...}} 가 시각적으로 squashed 되어 출력 (12r² 처럼 보임). 사후 점검에서 동일 결함 클래스가 parse_matrix 에도 잠재함을 확인 (matrix{a over b} 가 fraction 으로 인식되지 않음 — 진단 테스트로 검증). 정정: - try_consume_infix_over_atop() 헬퍼 추출 (parser.rs) - 6개 호출지점 통합 (parse_expression / parse_group / parse_cases / parse_pile / parse_eqalign × 2 / parse_matrix). 22줄 × 2 인라인 블록 → 헬퍼 호출로 DRY - tokenizer.skip_spaces 에 \n, \r 추가 (수식 스크립트 내 의미 없는 줄바꿈 무시) - 신규 회귀 테스트 9건 (tests/issue_505.rs): · CASES+EQALIGN 픽스처 4 (height ratio / fraction recognition / overlap / newline) · matrix bare OVER/ATOP · pile bare OVER/ATOP · cases bare ATOP · 좌결합 다중 OVER 체인 (5 row-collecting 경로 모두) · orphan OVER (top/bottom 없음) panic 안전성 검증: - 미적분03.hwp p5 pi=165 SVG y-scale 1.64 → 1.08 (수락 기준 ≤ 1.20 충족) - 27 fixtures × 344 pages 일괄 출력: panic 0, 새 극단값 도입 0 - 동일 3 fixture 비교: 극단 그룹 4 → 3 (1.637 제거 ★) - cargo test --lib 1102 통과 - cargo test --test issue_505 9 신규 통과 - cargo test --test issue_418/501 회귀 0 - PR edwardkim#396 회귀 0 (test_cases_korean_no_overlap 통과) - clippy 본 변경 영역 0건 비-목표 (별도 이슈): - 한컴 PDF baseline 비교 (Hancom COM 자동화 RPC 차단) - 인라인 CASES baseline 정렬 (페이지 6/7) — Phase A baseline 후 - LONGDIV 미구현 (P3 백로그) - svg_snapshot 5/6 사전 CRLF/LF 회귀 (main 동일) - parse_fraction / parse_fraction_in_range / parse_fraction_until_rbrace 3개 dead code 헬퍼 정리 (call site 0건) 산출물: - mydocs/plans/task_m100_505{,_impl}.md (계획서) - mydocs/working/task_m100_505_stage{1-4}.md (단계별 보고서) - mydocs/report/task_m100_505_report.md (최종 보고서) - mydocs/tech/all_in_one_parser_fidelity_strategy.md (전략) closes edwardkim#505
본질: parse_command 의 OVER/ATOP 폐기로 parse_cases / parse_pile / parse_eqalign / parse_matrix 의 row-collecting 루프에서 분수가 분실됨. PR #396 (#175 후속) 회귀 테스트가 다루지 못한 토폴로지. 발견 경로: all-in-one-parser fixture 1:1 정합화 전략의 A' 진단 → 미적분03.hwp p5 의 g(x)= {cases{{1} over {2} x ^{2} ...}} 가 시각적으로 squashed 되어 출력 (12r² 처럼 보임). 사후 점검에서 동일 결함 클래스가 parse_matrix 에도 잠재함을 확인 (matrix{a over b} 가 fraction 으로 인식되지 않음 — 진단 테스트로 검증). 정정: - try_consume_infix_over_atop() 헬퍼 추출 (parser.rs) - 6개 호출지점 통합 (parse_expression / parse_group / parse_cases / parse_pile / parse_eqalign × 2 / parse_matrix). 22줄 × 2 인라인 블록 → 헬퍼 호출로 DRY - tokenizer.skip_spaces 에 \n, \r 추가 (수식 스크립트 내 의미 없는 줄바꿈 무시) - 신규 회귀 테스트 9건 (tests/issue_505.rs): · CASES+EQALIGN 픽스처 4 (height ratio / fraction recognition / overlap / newline) · matrix bare OVER/ATOP · pile bare OVER/ATOP · cases bare ATOP · 좌결합 다중 OVER 체인 (5 row-collecting 경로 모두) · orphan OVER (top/bottom 없음) panic 안전성 검증: - 미적분03.hwp p5 pi=165 SVG y-scale 1.64 → 1.08 (수락 기준 ≤ 1.20 충족) - 27 fixtures × 344 pages 일괄 출력: panic 0, 새 극단값 도입 0 - 동일 3 fixture 비교: 극단 그룹 4 → 3 (1.637 제거 ★) - cargo test --lib 1102 통과 - cargo test --test issue_505 9 신규 통과 - cargo test --test issue_418/501 회귀 0 - PR #396 회귀 0 (test_cases_korean_no_overlap 통과) - clippy 본 변경 영역 0건 비-목표 (별도 이슈): - 한컴 PDF baseline 비교 (Hancom COM 자동화 RPC 차단) - 인라인 CASES baseline 정렬 (페이지 6/7) — Phase A baseline 후 - LONGDIV 미구현 (P3 백로그) - svg_snapshot 5/6 사전 CRLF/LF 회귀 (main 동일) - parse_fraction / parse_fraction_in_range / parse_fraction_until_rbrace 3개 dead code 헬퍼 정리 (call site 0건) 산출물: - mydocs/plans/task_m100_505{,_impl}.md (계획서) - mydocs/working/task_m100_505_stage{1-4}.md (단계별 보고서) - mydocs/report/task_m100_505_report.md (최종 보고서) - mydocs/tech/all_in_one_parser_fidelity_strategy.md (전략) closes #505
…X 분수 분실 정정 — cherry-pick @cskwork 2 commits) — closes #505 본 PR 은 외부 컨트리뷰터 @cskwork (Agentic-Worker) 의 첫 PR. PR #396 (Task #175) 이 다루지 못한 CASES+EQALIGN+MATRIX 중첩 토폴로지의 분수 분실 결함 정정. cherry-pick: - 7bcbe2c (12037a4): Task #505 CASES+EQALIGN+MATRIX 분수 분실 정정 · try_consume_infix_over_atop() 헬퍼 추출 (DRY) · 6 호출지점 통합 (parse_expression / parse_group / parse_cases / parse_pile / parse_eqalign × 2 / parse_matrix) · tokenizer.skip_spaces 에 \n, \r 추가 · tests/issue_505.rs 신규 (회귀 테스트 9건) - 1f65919 (4b1feea): Task #505 시각 판정용 fixture HWP 추가 · samples/issue-505-equations.hwp (4 fixture pi=151/165/196/227) · examples/build_issue_505_fixture.rs (재현 가능 빌더) 검증: - cargo test --lib 1110 passed - cargo test --test issue_505 9/9 통과 - cargo test --test issue_418/501 회귀 0 - cargo test --test svg_snapshot 6/6 통과 - cargo clippy --lib / --test issue_505 0 건 - WASM 빌드 4,461,235 bytes + rhwp-studio 동기화 시각 판정 (작업지시자 한컴 2010/2020 직접): - 1차 SVG export-svg 4/4 통과 - 2차 rhwp-studio web Canvas 4/4 통과
요약
기존 PR #388 을
devel기반으로 재작업한 PR 입니다.main으로 직접 올린 점 죄송합니다.1. TAC 높이에 HWP 권위값 사용 (#174)
eq.common.height(HWP 저장 값) 기준으로 설정2. 한글(CJK) 이탤릭 제거 (#175)
변경 파일 (4개)
src/renderer/svg.rs— 수식 SVG 배치 시 Y축 스케일링 추가src/renderer/layout/paragraph_layout.rs— HWP 수식 높이 우선 + 베이스라인 스케일 조정src/renderer/equation/svg_render.rs— CJK 수식 텍스트 이탤릭 제거src/renderer/equation/layout.rs— CJK 이탤릭 너비 보정 제외 + 회귀 테스트테스트
cargo test— 전체 통과cargo clippy -- -D warnings— 경고 0Closes #174, #175