한/글 export 기반 검증의 한계

이 프로젝트가 산출물을 “한/글이 실제로 받아들이는가”로 검증하는 방식은 강력하지만, 만능은 아닙니다. 특히 한/글이 export한 PDF에서 텍스트를 추출해 검사하는 방식에는 조용히 무너질 수 있는 사각지대가 있습니다. 이 문서는 그 한계와, 왜 픽셀(시각) 검증이 필요한지를 설명합니다.

두 종류의 오라클: 텍스트 기반과 픽셀 기반

한/글로 문서를 렌더해 검증하는 방법은 크게 둘입니다.

  1. 텍스트 기반: 한/글이 PDF로 export → PDF에서 단어/좌표를 추출(예: fitz get_text/words) → 기대값과 비교. 예컨대 목차 쪽번호 검증(src/hwpx/tools/toc_fidelity.py)이 “한/글 render → fitz words”로 캐시된 쪽번호와 실제 렌더 쪽번호를 대조합니다.

  2. 픽셀 기반: 한/글이 PDF로 export → PDF를 이미지로 래스터화 → 픽셀(잉크) 수준에서 검사. companion의 hwpx_automation.office.rendering 렌더 게이트가 이 방식입니다.

두 방식은 잡아내는 것이 다르고, 텍스트 기반에는 다음과 같은 한계가 있습니다.

텍스트 추출이 침묵 실패할 수 있다

실제 한/글에서 확인된 동작: 특정 문서에서는 한/글이 본문 텍스트를 문자(글리프)가 아니라 벡터 커브로 PDF에 export합니다. 이렇게 되면 PDF에는 시각적으로 글자가 그려져 있지만, 텍스트 추출은 아무 단어도 돌려주지 않습니다(추출 결과가 비어 있음).

이것이 위험한 이유는 실패가 조용하기 때문입니다. 텍스트 기반 게이트는 “추출된 텍스트 vs 기대 텍스트”를 비교하는데, 추출 결과가 비면 “비교할 것이 없음”이 되어 — 게이트를 어떻게 설계했느냐에 따라 — 아무 결함도 없는 것처럼 통과해 버릴 수 있습니다. 실제로는 렌더가 정상인지조차 확인하지 못했는데 말입니다. 이것이 “침묵 실패(silent pass)”입니다.

그래서 픽셀 검증이 필요하다

벡터 커브로 그려졌든 글리프로 그려졌든, 래스터화한 이미지에는 잉크가 남습니다. 픽셀 기반 검증은 텍스트 추출에 의존하지 않으므로 이 사각지대를 피합니다. hwpx_automation.office.rendering.page_qa가 잉크(픽셀) 수준에서 검사합니다:

  • 빈 페이지 탐지: 잉크 비율이 너무 낮으면 “의미 있는 렌더 잉크 없음”으로 실패 — 즉 텍스트가 있어야 할 자리에 아무것도 안 그려졌으면 잡습니다.

  • 겹침 탐지: 비정상적으로 높고 빽빽한 잉크 띠를 “붕괴되거나 겹친 텍스트”로 판정합니다(주석: Abnormally tall dense ink band suggests collapsed or overlapping text). 글자 겹침(→ lineseg.md 참고)은 텍스트 추출로는 안 보여도 픽셀로는 드러납니다.

  • 클리핑 탐지: 잉크가 페이지 가장자리에 닿으면 잘렸을 가능성으로 경고.

hwpx_automation.office.rendering.detectors는 before/after 두 렌더의 잉크 마스크 차이(diff_ratio)를 픽셀 단위로 계산해, 마스크 밖에서 잉크가 늘어난 영역을 잡습니다. 전부 텍스트 내용이 아니라 픽셀의 유무로 판정하므로 커브 export에 흔들리지 않습니다.

정직 규율: 검증하지 못한 것을 통과로 세지 않는다

이 프로젝트의 렌더 게이트는 “무엇을 검사했는가”와 “판정이 무엇인가”를 의도적으로 분리합니다. companion의 렌더 실행은 hwpx_automation.office.rendering.oracle, 중립 결과 계약 VisualReport는 core의 hwpx.quality.rendering이 소유합니다:

render_checked is True only when a Hancom render diff actually ran. Off-oracle (no Hancom / missing imaging deps) it is False and the report is a structural degrade, never a silent visual pass.

When render_checked is False it means “nothing could be verified” (optimistic-but-labelled), not “verified clean”.

즉 한/글 오라클이 없거나 이미지 처리 의존성이 없으면, 결과는 “구조만 봤음(render_checked=False)”으로 낮춰 라벨링되지, “시각적으로 깨끗함”으로 위장되지 않습니다. 텍스트 기반 목차 검증도 마찬가지로, 오라클이 없으면 구조 검증으로 degrade합니다(toc_fidelity.py).

이 규율은 실측 코퍼스 발행에도 그대로 적용됩니다(docs/corpus-metrics.md). 예컨대 변경추적 문서의 PDF export는 한/글 자체가 거부하는데, 이런 건은 render_unavailable 버킷으로 분리해 발행하고 절대 pass로 집계하지 않습니다. “낮은 숫자도 그대로 발행”하는 것이 이 프로젝트의 원칙입니다.

함정 하나 더: refresh와 render의 세션 분리

관련해서 실측된 함정: 필드(목차 등)를 재생성 중인 한/글 세션에서 곧바로 PDF export를 시도하면 이 한/글 빌드가 크래시합니다(잘린 PDF 후 프로세스 사망). 그래서 “필드 재계산”과 “렌더”는 별개 세션으로 분리됩니다(→ toc-dirty.md 참고). 오라클을 자동화할 때 알아 두면 좋은 제약입니다.

실전 요약

  • 한/글 export의 텍스트 추출 결과가 비어 있다고 해서 “문서에 글자가 없다”는 뜻이 아닙니다 — 벡터 커브로 그려졌을 수 있습니다. 텍스트 기반 검사만 믿으면 침묵 실패에 빠질 수 있습니다.

  • 렌더 정상 여부는 픽셀(잉크) 수준으로 검증하세요(빈 페이지·겹침·클리핑). 이 방식은 글리프/커브 여부와 무관합니다.

  • 검증하지 못한 것은 통과가 아니라 “미검증(render_checked=False)”으로 라벨링하는 것이 정직합니다. 이 프로젝트의 게이트와 코퍼스 리포트가 그 규율을 따릅니다.