# 한/글 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](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](toc-dirty.md) 참고). 오라클을 자동화할 때 알아 두면 좋은 제약입니다. ## 실전 요약 - 한/글 export의 텍스트 추출 결과가 비어 있다고 해서 "문서에 글자가 없다"는 뜻이 아닙니다 — 벡터 커브로 그려졌을 수 있습니다. 텍스트 기반 검사만 믿으면 침묵 실패에 빠질 수 있습니다. - 렌더 정상 여부는 **픽셀(잉크) 수준**으로 검증하세요(빈 페이지·겹침·클리핑). 이 방식은 글리프/커브 여부와 무관합니다. - 검증하지 못한 것은 통과가 아니라 "미검증(`render_checked=False`)"으로 라벨링하는 것이 정직합니다. 이 프로젝트의 게이트와 코퍼스 리포트가 그 규율을 따릅니다.