Skip to content

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

Closed
oksure wants to merge 2 commits into
edwardkim:develfrom
oksure:contrib/equation-rendering-improvements-v2
Closed

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

Conversation

@oksure
Copy link
Copy Markdown
Contributor

@oksure oksure commented Apr 27, 2026

요약

기존 PR #388devel 기반으로 재작업한 PR 입니다. main 으로 직접 올린 점 죄송합니다.

1. TAC 높이에 HWP 권위값 사용 (#174)

  • 인라인 수식 높이를 eq.common.height (HWP 저장 값) 기준으로 설정
  • X/Y 스케일링 동시 적용으로 수식 bbox 정확도 향상
  • 큰 수식이 인접 행과 겹치는 문제 해결

2. 한글(CJK) 이탤릭 제거 (#175)

  • 수식 내 CJK 문자에 이탤릭 스타일링 및 너비 보정 비적용
  • CASES 표현식에서 한글 행이 겹치는 문제 해결
  • 회귀 테스트 추가 (CASES overlap / width)

변경 파일 (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 — 경고 0

Closes #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 23:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread src/renderer/equation/layout.rs Outdated
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);
}
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

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.

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 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
);

Copilot uses AI. Check for mistakes.
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,
));
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

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.

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)
};
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
1. test_cases_korean_no_overlap: if-let silent pass → match + panic!
   으로 구조 변경 시 테스트가 명시적으로 실패하도록 개선.
   실제 구조가 Row[..., Paren{cases}]임을 확인하고 테스트 수정.

2. canvas_render.rs: CJK 수식 텍스트 이탤릭 제거를 SVG 렌더러와
   동일하게 Canvas 렌더러에도 적용 (백엔드 일관성).
@oksure
Copy link
Copy Markdown
Contributor Author

oksure commented Apr 28, 2026

Copilot 리뷰 피드백 반영했습니다 (dbaa74b):

  1. 테스트 명시적 실패 (layout.rs) — if let silent pass → match + panic!으로 변경. 실제로 전체 수식이 Row[subscript, =, Paren{cases}] 구조임을 확인하고 테스트가 올바르게 CASES Paren을 찾아 overlap 검증하도록 수정.

  2. Canvas CJK 이탤릭 일관성 (canvas_render.rs) — SVG 렌더러와 동일한 CJK 범위 검사를 Canvas 렌더러에도 적용. LayoutKind::Text에서 CJK 포함 시 italic=falseset_font 호출.

  3. Helper 추출 (paragraph_layout.rs) — 인라인 수식 배치 로직 중복 해소 제안은 유효하나, 이 PR scope에서는 보류합니다. 향후 리팩토링 시 반영하겠습니다.

cargo test 전체 통과, cargo clippy -- -D warnings 경고 0.

edwardkim added a commit that referenced this pull request Apr 28, 2026
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>
edwardkim added a commit that referenced this pull request Apr 28, 2026
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>
edwardkim added a commit that referenced this pull request Apr 28, 2026
증상:
- 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>
edwardkim added a commit that referenced this pull request Apr 28, 2026
- PR #396 (수식 렌더링 개선, @oksure) 옵션 B cherry-pick + 메인테이너 후속 정정 (3건) 완료
- 검토 / 처리 보고서 추가
- 오늘할일 갱신

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
edwardkim added a commit that referenced this pull request Apr 28, 2026
@edwardkim
Copy link
Copy Markdown
Owner

완료. `devel` 에 cherry-pick (작성자 attribution 보존) + 메인테이너 후속 정정으로 머지됨 (commit `0d51736`).

이슈 #174, #175 의 SVG 경로 정정이 적용됐습니다.

머지 commit

작성자 attribution 보존:

  • `2e62ef0` (← `5cf2b79`, @oksure) — 수식 렌더링 개선 (TAC 높이 + 한글 이탤릭)
  • `9d4669e` (← `dbaa74b`, @oksure) — Copilot 리뷰 반영

메인테이너 후속 정정 (Canvas 경로 SVG 일치화):

  • `7048d3e` — Canvas 분수선 y `- AXIS_HEIGHT` 정정
  • `dff4b07` — web_canvas.rs Equation X/Y 스케일 적용
  • `d47b7c7` — Canvas Limit `fi = fs` 정정

후속 정정 정황

작업지시자가 web 에디터 (Canvas 경로) 와 SVG 경로 시각 비교에서 다음 결함 발견:

  • Canvas 의 분수선이 분모와 겹침
  • Canvas 의 `lim` 글자가 비정상으로 큼
  • Canvas 가 SVG 의 Y축 스케일링 정정을 적용 안 함 (수식이 bbox 와 다른 영역에 그려짐)

각 결함의 원인은 Canvas 경로가 SVG 경로와 일관되지 않은 코드 조각 (`font_size_from_box` 사용, scale 누락 등). 본 PR 후속 정정으로 SVG 경로와 일치화.

검증

  • `cargo test --lib`: 1031 passed (1029 → +2 작성자 신규)
  • `cargo test --test svg_snapshot`: 6/6
  • `cargo clippy --lib -- -D warnings`: warning 0건
  • `cargo check --target wasm32-unknown-unknown --lib`: 통과
  • WASM 빌드: 4,106,811 bytes
  • 작업지시자 시각 판정 통과 (web 에디터 + SVG 일치)

참고

  • 동일 작성자 머지 PR: #395 (그림 밝기/대비)
  • 본 PR 의 부수 발견 (Canvas 경로 일관성) 은 후속 정정으로 흡수됨

좋은 기여 감사합니다.

@edwardkim edwardkim closed this Apr 28, 2026
cskwork added a commit to cskwork/rhwp that referenced this pull request May 1, 2026
본질: 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
edwardkim pushed a commit that referenced this pull request May 3, 2026
본질: 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
edwardkim added a commit that referenced this pull request May 3, 2026
…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 통과
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