두 번째 앱 출시기 1 — 한글이 검색되는 PDF 만들기 - OCR 인식 좌표와 글자층
첫 번째 앱 Stock Calculator에 이어 두 번째 앱 Pocket PDF를 만들었습니다. 이미지를 PDF로 변환하면서 텍스트를 인식해 투명 글자층으로 얹는 핵심 엔진 개발과 한글 유니코드 문제를 해결한 과정을 정리합니다.
첫 번째 앱인 물타기 계산기(Stock Calculator)를 플레이 스토어 테스트 트랙에 올린 뒤, 곧바로 두 번째 제품인 ‘Pocket PDF’ 개발에 착수했습니다.
스마트폰 카메라로 문서를 촬영하거나 영수증, 계약서 이미지를 불러와 깨끗한 PDF 문서로 만들고, 이미지 안의 글자를 검색하고 복사할 수 있게 해주는 휴대용 PDF 유틸리티입니다.
첫 번째 편에서는 Pocket PDF의 가장 핵심이자 가장 큰 기술적 도전이었던 ‘검색 가능한 PDF(Searchable PDF) 엔진’ 구축 과정을 다룹니다.
이미지 위에 보이지 않는 글자 올리기
종이 문서 사진을 단순 PDF로 묶는 것은 쉽습니다. 하지만 사용자가 기대하는 진짜 디지털 문서는 “원하는 단어를 검색(Ctrl+F)하고, 손가락으로 긁어 복사할 수 있는 문서”입니다.
이를 구현하는 핵심 원리는 다음과 같습니다:
- ML Kit OCR 엔진 실행: 이미지에서 텍스트 블록, 라인, 단어들의 바운딩 박스(Bounding Box) 좌표와 인식된 문자열을 추출합니다.
- 좌표계 스케일링: 원본 이미지의 픽셀 좌표를 PDF 페이지의 포인트(Point, 72 DPI) 좌표계로 정확히 변환합니다.
- 투명 텍스트 레이어 렌더링 (
InvisibleTextLayer): 배경에는 스캔 이미지를 그대로 그리고, 그 위에 투명 렌더링 모드(RenderingMode.NEITHER_FILL_NOR_STROKE)로 글자를 정확한 위치에 얹습니다.
흩어져 있던 좌표 계산을 한곳으로 모으다
처음에는 ‘이미지 → PDF 변환기’와 ‘실시간 문서 스캐너’ 두 곳에서 각자 좌표 계산 함수를 들고 있었습니다.
두 화면의 코드를 하나로 모으는 리팩토링을 진행하다가 충격적인 버그를 발견했습니다.
// InvisibleTextLayer 내부의 문자 정제 함수
private fun sanitize(text: String): String {
// 과거 누군가 넣어둔 영문 전용 필터
return text.map { char ->
if (char.code < 256) char else ' '
}.joinToString("")
}
‘코드값 256 이상의 문자를 전부 공백으로 바꾼다’. 영문 ASCII만 염두에 두고 짰던 코드가 그대로 남아 있었습니다.
한글 유니코드는 U+AC00부터 시작합니다. 화면에서는 분명히 OCR이 끝나 검색 가능한 PDF가 만들어졌다고 알렸지만, 한국 주 시장에서 한글은 단 한 글자도 검색되지 않고 있었던 것입니다.
중복 코드를 걷어내고 InvisibleTextLayer 단일 모듈로 통일하지 않았다면 영원히 발견하지 못했을 버그였습니다.
ToUnicode 매핑과 380KB 초경량 폰트
버그를 잡은 뒤에도 문제는 끝나지 않았습니다. 앞선 안드로이드 연재 11편에서 다룬 것처럼, PDF 뷰어가 한글을 인식하려면 올바른 폰트의 /ToUnicode CMap이 필요했습니다.
우리는 Pretendard(OFL) 폰트에서 현대 한글 2,350자와 필수 기호만 추출하는 tools/bake-ocr-font.py 스크립트를 작성하여 380KB의 초경량 OTF 서브셋을 구워 패키징했습니다.
그리고 JVM 상에서 pdfbox-android로 생성된 PDF의 ToUnicode를 역추출하는 단위 테스트를 작성해 완벽한 한글 검색을 최종 증명했습니다.
결국 바꾼 것
- 중복을 제거하는 진짜 가치는 코드 감소가 아니라 ‘숨은 거짓말을 발견하는 것’입니다. 두 군데로 나뉘어 있을 때는 아무도 그 함수를 다시 읽지 않았습니다.
- 기능이 동작하는 척하는 것과 진짜 되는 것은 다릅니다. OCR 좌표가 맞고 폰트가 실렸어도, 뷰어의 역추출 테스트를 통과하기 전까지는 완성된 것이 아닙니다.
- 다음 편에서는 복잡하게 꼬여 있던 인앱 결제와 광고 수익화 모델을 단순하고 정직하게 뜯어고친 과정을 다룹니다.