상세 컨텐츠

본문 제목

브라우저에서 폰트를 찾는 방법 : OCR과 픽셀 매칭을 이용한 로컬 Font Finder

Developer/개발일지

by cepiloth 2026. 9. 6. 13:54

본문

728x90

URL - https://image.polarisofficetools.com/font-finder/

브라우저 안에서 폰트를 찾는 방법

이미지에 쓰인 폰트가 무엇인지 알아내는 일은 생각보다 어렵다. OCR은 글자가 무엇인지는 알려주지만, 그 글자가 어떤 서체로 그려졌는지는 알려주지 않는다. 서버 기반 이미지 검색이나 상용 API를 사용할 수도 있지만, 비용과 개인정보 보호라는 현실적인 제약이 남는다.

POLARIS OFFICE TOOLS+의 이미지 폰트 찾기는 이 문제를 브라우저 안에서 해결한다. PaddleOCR.js가 텍스트와 위치를 찾고, OpenCV.js가 비교 가능한 글립 마스크를 만든다. 이후 Canvas로 후보 폰트를 여러 조건에서 렌더링하고 Pixelmatch를 포함한 복합 점수로 상위 5개를 선정한다. 원본 이미지는 서버로 전송하지 않는다.

이 글의 점수는 특정 폰트임을 보증하는 식별 신뢰도가 아니다. 현재 브라우저에서 비교할 수 있었던 후보군 안의 상대적 픽셀 유사도다.

한눈에 보는 결과

결과 카드에는 순위, 폰트 이름, 자체 호스팅 또는 시스템 폰트 여부, 추정 굵기, 상대 유사도와 세부 지표가 표시된다. 자체 호스팅 및 오픈소스 폰트는 다운로드 페이지로, 운영체제에 포함된 상용 폰트는 Microsoft 또는 Apple의 공식 정보 페이지로 연결한다.

우리가 풀고자 한 문제

폰트 비교 기능을 만들 때 단순히 이미지 전체와 Canvas 결과를 겹쳐 보면 다음 문제가 발생한다.

  1. OCR 영역이 기울어졌거나 사다리꼴로 왜곡되어 있다.
  2. 배경이 흰색이라는 보장이 없고 글자 극성도 다르다.
  3. OCR이 인식한 문자열이 틀리면 모든 후보가 불리해진다.
  4. 같은 폰트라도 크기, 자간, 굵기와 기준선 위치에 따라 픽셀 차이가 커진다.
  5. 시스템에 설치되지 않은 폰트는 CSS fallback으로 조용히 대체된다.
  6. 한글 폰트 전체를 한꺼번에 받으면 초기 다운로드가 커진다.

따라서 이 기능은 OCR, 이미지 정규화, 폰트 로딩, 탐색 최적화, 복합 점수와 실패 격리를 하나의 파이프라인으로 묶어야 했다.

전체 아키텍처

 

UI 스레드는 파일 입력, 영역 선택, 폰트 로딩과 결과 표시를 담당한다. OCR과 OpenCV 전처리는 ocr.worker.ts, 반복적인 마스크 점수 계산은 fontMatch.worker.ts에서 실행한다. 무거운 작업을 두 Worker로 나눈 이유는 비교 중에도 취소 버튼, 진행 상태와 키보드 탐색이 멈추지 않게 하기 위해서다.

OCR 모델, ONNX Runtime Web과 OpenCV WASM, WOFF2 폰트 파일은 모두 서비스와 동일한 출처에서 제공한다. 이미지 데이터는 Worker 사이에서만 이동하며 외부 API로 전송되지 않는다.

처리 파이프라인

1. 입력 크기를 먼저 제한한다

지원 형식은 PNG, JPEG, WebP, BMP이며 최대 파일 크기는 25MB다. 디코딩한 이미지의 긴 변이 2,400px를 넘으면 Canvas에서 비율을 유지한 채 축소한다. OCR에 불필요하게 큰 픽셀을 넘기지 않아 메모리와 처리 시간을 제한한다.

2. OCR은 문자열과 폴리곤을 함께 반환한다

PaddleOCR.js의 한국어 PP-OCRv5 모바일 모델을 ONNX Runtime Web의 WASM 백엔드에서 실행한다. 결과는 문자열만이 아니라 네 꼭짓점으로 구성된 폴리곤과 인식 점수를 포함한다.

사용자는 이미지 위의 폴리곤 또는 키보드로 탐색 가능한 영역 목록에서 하나를 고른다. OCR 문자열은 비교 전에 수정할 수 있다. 공백을 제외해 2글자 미만이면 비교를 막고, 4글자 미만이면 후보 간 차이가 작을 수 있음을 알린다.

3. OCR 영역을 96px 글립 마스크로 만든다

선택한 네 점에서 원근 변환 행렬을 계산하고 사각 이미지로 보정한다. 이후 다음 순서로 비교 샘플을 만든다.

원근 보정
  → 회색조 변환
  → 적응형 Gaussian 이진화 또는 작은 영역의 Otsu 이진화
  → 어두운 픽셀 비율을 이용한 전경 극성 판정
  → 전경의 tight crop
  → 글립 높이 80px + 여백을 포함한 96px 캔버스 정규화
원근 보정 → 회색조 변환 → 적응형 Gaussian 이진화 또는 작은 영역의 Otsu 이진화 → 어두운 픽셀 비율을 이용한 전경 극성 판정 → 전경의 tight crop → 글립 높이 80px + 여백을 포함한 96px 캔버스 정규화

출력 폭은 문자열 길이에 따라 96~640px 범위로 제한된다. 이 단계가 끝나면 배경 색과 촬영 각도의 영향을 크게 줄인 0과 1의 마스크를 얻는다.

4. 필요한 폰트 조각만 늦게 로드한다

후보는 한글 팩 18종과 라틴 팩 12종, 총 30종의 자체 호스팅 Google Fonts 계열로 구성된다. 텍스트에 한글이 있는지 먼저 판단해 필요한 스크립트 팩만 동적 import한다.

각 폰트는 WOFF2와 unicode-range 조각으로 제공된다. 브라우저는 실제 문자열에 필요한 조각만 가져오고, 코드는 document.fonts.load()document.fonts.check()가 완료된 뒤 렌더링을 시작한다. 빌드 검사에서는 라이선스, lockfile 무결성, 굵기별 CSS와 대표 문자열 기준 30MB 예산을 확인한다.

시스템 폰트는 임의로 열거하지 않는다. Windows, Apple, Android/Linux의 대표 폰트 allowlist만 검사한다. 후보를 monospaceserif 두 fallback으로 각각 렌더링했을 때 미설치 기준 글꼴과 픽셀이 같으면 해당 후보를 제외한다. 권한과 브라우저 지원 범위가 제한적인 queryLocalFonts()는 사용하지 않았다.

렌더링 조건을 하나로 고정하지 않은 이유

원본 글자가 64px로 보인다고 해서 Canvas의 64px이 같은 실제 글립 높이를 만드는 것은 아니다. 글꼴마다 ascent, descent와 내부 여백이 다르기 때문이다. 그래서 이 구현은 이진 탐색으로 실제 잉크 높이를 맞춘 다음 아래 조합을 만든다.

  • 크기 비율: 94%, 100%, 106%
  • 자간: -0.04em, 0, +0.04em
  • 굵기: 해당 폰트가 실제 제공하는 400 또는 700
  • 위치: x와 y 각각 -3px, 0, +3px

크기와 자간 조합은 굵기당 9개다. 모든 조합에 비싼 점수를 계산하지 않고, 먼저 빠른 Dice overlap으로 상위 4개를 고른다. 이 네 조합에만 3×3 위치 탐색과 전체 점수를 적용하므로 후보 하나당 최대 36회의 정밀 점수 계산으로 제한된다.

하나의 지표만 사용하지 않은 이유

픽셀 일치율 하나만 사용하면 1~2px 이동에도 점수가 크게 떨어진다. 반대로 겹치는 면적만 보면 획 끝과 곡선의 차이를 놓치기 쉽다. 최종 점수는 세 지표의 역할을 나눴다.

Dice overlap 55%

두 전경 마스크가 차지하는 면적의 교집합을 본다.

Dice = 2 × |원본 ∩ 후보| / (|원본| + |후보|)​

Edge-distance similarity 30%

원본과 후보 각각에 Manhattan distance transform을 적용하고 양방향 평균 거리를 구한다. 평균 거리는 exp(-distance / 3)으로 0~1 유사도로 변환한다. 작은 위치 오차를 완충하면서 획의 윤곽이 가까운지를 본다.

Pixelmatch agreement 15%

두 마스크를 흑백 RGBA로 바꾸고 Pixelmatch의 threshold 0.15, 안티앨리어싱 포함 조건으로 불일치 픽셀을 계산한다. 이는 최종적인 픽셀 수준 검증 역할을 한다.

raw = 0.55 × Dice
    + 0.30 × EdgeSimilarity
    + 0.15 × PixelAgreement

score = clamp(raw × 100 - AspectRatioPenalty, 0, 100)

글립 bounding box의 종횡비가 원본과 다르면 로그 비율 기반 패널티를 최대 10점까지 차감한다. 최종 표시값은 소수점 첫째 자리까지 반올림한다.

실패는 후보 단위로 격리한다

브라우저, 운영체제와 네트워크 캐시는 사용자마다 다르다. 특정 폰트 조각이 손상되거나 시스템 후보가 실제로 렌더링되지 않을 수 있다. 이런 실패 하나 때문에 전체 비교가 중단되면 도구로서 쓸 수 없다.

각 후보의 로딩과 점수 계산은 독립적으로 실패할 수 있으며, 실패한 후보만 skippedCount에 포함한다. 비교 가능한 후보가 하나라도 있으면 나머지 결과로 Top 5를 만든다. 모든 후보가 실패했을 때만 재시도 가능한 전체 오류를 표시한다.

취소 또는 새 이미지 선택 시 요청 세대 번호를 증가시켜 이전 응답을 무시하고, Worker와 타이머를 정리한다. 생성한 객체 URL도 React effect cleanup에서 해제한다.

결과 화면과 다운로드 연결

정렬은 점수 내림차순이며, 같은 점수에서는 폰트명으로 정렬해 결과를 결정적으로 유지한다. 상위 5개 카드는 같은 크기의 캔버스에서 원본과 후보를 비교할 수 있게 한다.

다운로드 링크도 폰트 성격에 따라 구분했다.

  • Google Fonts 및 오픈소스 폰트: 해당 폰트 다운로드 또는 릴리스 페이지
  • Windows·Apple 번들 폰트: 라이선스를 오해하지 않도록 공식 정보 페이지

모든 외부 링크는 새 탭에서 열며 noopener noreferrer를 적용한다.

검증한 항목

  • 동일 마스크 100점, 이동 허용, 빈 마스크 거부와 0~100 정규화
  • 동점 정렬의 결정성
  • 스크립트별 지연 로드와 실제 지원 굵기 선택
  • 시스템 fallback 판별과 폰트 로드 캐시 재사용
  • 한글·영문 이미지의 실제 OCR 및 Top 5 결과
  • 영역 선택, 문자열 수정, 취소, 재실행과 부분 실패
  • 키보드 영역 탐색과 aria-live 진행 상태
  • 320px, 390px, 데스크톱의 가로 overflow 회귀
  • 기존 이미지 OCR 메시지와 화면의 호환성
  • QUICK TOUR 6단계, 저장된 표시 설정과 WebM 녹화

집중 Playwright 테스트와 개발 빌드를 통과했으며, 폰트 자산 검사도 빌드 전에 자동 실행된다.

현재 한계

v1은 수평에 가까운 한글·라틴 단색 인쇄 글자를 대상으로 한다. 다음 조건은 결과 품질을 보장하지 않는다.

  • 손글씨와 세로쓰기
  • 강한 기울임꼴, 윤곽선, 그림자와 질감 효과
  • 심하게 왜곡된 로고
  • 한 OCR 영역 안에 여러 폰트가 섞인 경우
  • 카탈로그에 없는 상용·로컬 폰트
  • 글자가 2~3자로 매우 짧은 경우

이 한계 때문에 결과를 “정답 확률”이 아니라 “현재 후보군 안의 상대 유사도”로 표현한다.

다음 단계

정확도를 더 높이려면 문자 단위 분할과 특징 벡터 검색을 결합할 수 있다. 픽셀 비교로 전체 후보를 순회하기 전에 가벼운 임베딩으로 후보를 줄이고, 상위 후보에만 현재의 정밀 점수를 적용하는 방식이다. 여러 배경과 촬영 조건을 포함한 회귀 데이터셋, variable font 축 탐색과 이탤릭·세로쓰기 전용 정규화도 후속 과제다.

다만 v1의 목표는 거대한 모델이 아니라 비용 없이, 이미지를 외부로 보내지 않고, 결과 근거를 눈으로 비교할 수 있는 폰트 탐색기다. 브라우저의 OCR, Canvas와 표준 Font Loading API만으로도 이 목표에 충분히 가까이 갈 수 있었다.

참고 자료

728x90
반응형

관련글 더보기

댓글 영역