aliceblue11's picture
Update app.py
7df49bf verified
raw
history blame
23.1 kB
import gradio as gr
import pandas as pd
import io
import re
from difflib import get_close_matches
from datetime import datetime
import tempfile
import os
# 확장된 한국어 폰트 라이선스 데이터베이스
FONT_LICENSE_DB = {
# 네이버 폰트
"나눔고딕": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "네이버",
"provider_url": "https://hangeul.naver.com/2017/nanum",
"notes": "웹폰트 사용 가능"
},
"나눔바른고딕": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "네이버",
"provider_url": "https://hangeul.naver.com/2017/nanum",
"notes": "나눔고딕 개선 버전"
},
"나눔명조": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "네이버",
"provider_url": "https://hangeul.naver.com/2017/nanum",
"notes": "명조체"
},
"나눔손글씨": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "네이버",
"provider_url": "https://hangeul.naver.com/2017/nanum",
"notes": "손글씨 스타일"
},
"나눔펜": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "네이버",
"provider_url": "https://hangeul.naver.com/2017/nanum",
"notes": "펜 스타일"
},
# 배달의민족 폰트
"배달의민족 주아": {
"license": "커스텀 무료",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "불가능",
"provider": "배달의민족",
"provider_url": "https://www.woowahan.com/fonts",
"notes": "BI 제작 시 사용 금지, 폰트 판매 금지"
},
"배달의민족 도현": {
"license": "커스텀 무료",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "불가능",
"provider": "배달의민족",
"provider_url": "https://www.woowahan.com/fonts",
"notes": "BI 제작 시 사용 금지, 폰트 판매 금지"
},
"배달의민족 기랑해랑": {
"license": "커스텀 무료",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "불가능",
"provider": "배달의민족",
"provider_url": "https://www.woowahan.com/fonts",
"notes": "BI 제작 시 사용 금지, 폰트 판매 금지"
},
# 서울시 폰트
"서울남산체": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "서울시",
"provider_url": "https://www.seoul.go.kr/solution/font.do",
"notes": "서울시 공식 폰트"
},
"서울한강체": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "서울시",
"provider_url": "https://www.seoul.go.kr/solution/font.do",
"notes": "서울시 공식 폰트"
},
# Google/Adobe 폰트
"Noto Sans KR": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "Google",
"provider_url": "https://fonts.google.com/noto",
"notes": "Google Fonts 제공"
},
"Noto Serif KR": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "Google",
"provider_url": "https://fonts.google.com/noto",
"notes": "Google Fonts 명조체"
},
"본고딕": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "Adobe/Google",
"provider_url": "https://fonts.google.com/noto",
"notes": "Noto Sans CJK와 동일"
},
# 오픈소스 폰트
"Pretendard": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "Kil Hyung-jin",
"provider_url": "https://github.com/orioncactus/pretendard",
"notes": "시스템 UI 최적화 폰트"
},
"SUIT": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "SUNN",
"provider_url": "https://github.com/sunn-us/SUIT",
"notes": "본고딕 기반 개선"
},
"Spoqa Han Sans": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "스포카",
"provider_url": "https://github.com/spoqa/spoqa-han-sans",
"notes": "Source Han Sans 기반"
},
# IBM 폰트
"IBM Plex Sans KR": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "IBM",
"provider_url": "https://fonts.google.com/specimen/IBM+Plex+Sans+KR",
"notes": "IBM 공식 폰트"
},
# 카카오 폰트
"카카오 Regular": {
"license": "커스텀 무료",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "불가능",
"provider": "카카오",
"provider_url": "https://kadx.co.kr/266",
"notes": "카카오 공식 폰트"
},
# 티몬 폰트
"TmoneyRoundWind": {
"license": "커스텀 무료",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "불가능",
"provider": "티몬",
"provider_url": "https://brunch.co.kr/@creative/32",
"notes": "티몬 공식 폰트"
},
# 넥슨 폰트
"NEXON Lv1 Gothic": {
"license": "커스텀 무료",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "불가능",
"provider": "넥슨",
"provider_url": "https://www.nexon.com/Home/Game/font",
"notes": "넥슨 공식 폰트"
},
"NEXON Lv2 Gothic": {
"license": "커스텀 무료",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "불가능",
"provider": "넥슨",
"provider_url": "https://www.nexon.com/Home/Game/font",
"notes": "넥슨 공식 폰트"
},
# 마켓컬리 폰트
"MarketKurly": {
"license": "커스텀 무료",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "불가능",
"provider": "마켓컬리",
"provider_url": "https://thefaceshop.com/marketkurly-font",
"notes": "마켓컬리 공식 폰트"
},
# 윤디자인 폰트 (상업용)
"윤고딕": {
"license": "상업적 라이선스 필요",
"commercial_free": "❌ 유료",
"attribution": "해당없음",
"modification": "라이선스에 따라",
"provider": "윤디자인그룹",
"provider_url": "https://www.yoondesign.com",
"notes": "개인 사용만 무료, 상업적 사용 시 라이선스 구매 필요"
},
"윤명조": {
"license": "상업적 라이선스 필요",
"commercial_free": "❌ 유료",
"attribution": "해당없음",
"modification": "라이선스에 따라",
"provider": "윤디자인그룹",
"provider_url": "https://www.yoondesign.com",
"notes": "개인 사용만 무료, 상업적 사용 시 라이선스 구매 필요"
},
# 산돌폰트 (상업용)
"산돌고딕": {
"license": "상업적 라이선스 필요",
"commercial_free": "❌ 유료",
"attribution": "해당없음",
"modification": "라이선스에 따라",
"provider": "산돌커뮤니케이션",
"provider_url": "https://www.sandoll.co.kr",
"notes": "개인 사용만 무료, 상업적 사용 시 라이선스 구매 필요"
},
"산돌명조": {
"license": "상업적 라이선스 필요",
"commercial_free": "❌ 유료",
"attribution": "해당없음",
"modification": "라이선스에 따라",
"provider": "산돌커뮤니케이션",
"provider_url": "https://www.sandoll.co.kr",
"notes": "개인 사용만 무료, 상업적 사용 시 라이선스 구매 필요"
},
# 공공기관 폰트
"한국관광공사체": {
"license": "공공누리 제1유형",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "한국관광공사",
"provider_url": "https://kto.visitkorea.or.kr/kor/notice/data/storybook/font.kto",
"notes": "공공누리 제1유형 (출처표시)"
}
}
def clean_font_name(font_name):
"""폰트 이름 정리 (확장자 제거, 공백 정리 등)"""
# 확장자 제거
font_name = re.sub(r'\.(ttf|otf|ttc|woff|woff2)$', '', font_name, flags=re.IGNORECASE)
# 파일명에서 폰트명 추출 패턴들
patterns = [
r'(.+?)[-_\s]?(Regular|Bold|Light|Medium|Thin|Black|Heavy|ExtraBold|SemiBold|Italic)',
r'(.+?)[-_\s]?\d+', # 숫자 제거
r'(.+?)[-_\s]?(Kr|KR|Korean)', # 언어 코드 제거
r'(.+?)[-_\s]?(ttf|TTF)', # ttf 제거
]
cleaned = font_name.strip()
for pattern in patterns:
match = re.search(pattern, cleaned, re.IGNORECASE)
if match:
cleaned = match.group(1).strip()
break
# 특수 문자 정리
cleaned = re.sub(r'[-_]+', ' ', cleaned).strip()
return cleaned
def find_font_license(font_name):
"""폰트 라이선스 정보 찾기"""
cleaned_name = clean_font_name(font_name)
# 1. 정확한 매칭
if cleaned_name in FONT_LICENSE_DB:
return FONT_LICENSE_DB[cleaned_name]
# 2. 부분 매칭 (포함 관계)
for db_font, info in FONT_LICENSE_DB.items():
if db_font.lower() in cleaned_name.lower() or cleaned_name.lower() in db_font.lower():
return info
# 3. 특별한 패턴 매칭
special_patterns = {
r'nanum|나눔': "나눔고딕",
r'baemin|배민|배달의민족': "배달의민족 주아",
r'seoul|서울': "서울남산체",
r'pretendard': "Pretendard",
r'noto.*sans': "Noto Sans KR",
r'noto.*serif': "Noto Serif KR",
r'ibm.*plex': "IBM Plex Sans KR",
r'spoqa': "Spoqa Han Sans",
r'nexon': "NEXON Lv1 Gothic",
r'yoon|윤': "윤고딕",
r'sandoll|산돌': "산돌고딕"
}
for pattern, matched_font in special_patterns.items():
if re.search(pattern, cleaned_name.lower()):
if matched_font in FONT_LICENSE_DB:
return FONT_LICENSE_DB[matched_font]
# 4. 유사도 매칭
matches = get_close_matches(cleaned_name, FONT_LICENSE_DB.keys(), n=1, cutoff=0.6)
if matches:
return FONT_LICENSE_DB[matches[0]]
# 5. 없는 경우 기본값
return {
"license": "❓ 확인 필요",
"commercial_free": "❓ 확인 필요",
"attribution": "❓ 확인 필요",
"modification": "❓ 확인 필요",
"provider": "❓ 확인 필요",
"provider_url": "",
"notes": "라이선스 정보를 찾을 수 없습니다. 제작사 공식 사이트에서 확인하세요."
}
def parse_font_list(file_content):
"""업로드된 폰트 목록 파싱"""
try:
# 여러 인코딩 시도
if isinstance(file_content, bytes):
encodings = ['utf-8', 'cp949', 'euc-kr', 'latin1']
for encoding in encodings:
try:
file_content = file_content.decode(encoding)
break
except:
continue
# 텍스트 내용을 줄별로 분리
lines = file_content.strip().split('\n')
fonts = []
for line in lines:
line = line.strip()
if line and not line.startswith('#') and not line.startswith('//'): # 빈 줄과 주석 제외
fonts.append(line)
return fonts
except Exception as e:
return [f"파일 파싱 오류: {str(e)}"]
def analyze_fonts(file_content):
"""폰트 목록 분석 및 라이선스 정보 생성"""
try:
font_list = parse_font_list(file_content)
if not font_list or (len(font_list) == 1 and font_list[0].startswith("파일 파싱 오류")):
return None, "파일을 올바르게 읽을 수 없습니다. UTF-8, CP949, EUC-KR 인코딩을 확인해주세요."
results = []
for font_file in font_list:
font_name = clean_font_name(font_file)
license_info = find_font_license(font_name)
results.append({
"원본 파일명": font_file,
"폰트명": font_name,
"라이선스": license_info["license"],
"상업적 사용": license_info["commercial_free"],
"출처 표시": license_info["attribution"],
"수정 가능": license_info["modification"],
"제공처": license_info["provider"],
"제공처 URL": license_info["provider_url"],
"비고": license_info["notes"]
})
# DataFrame으로 변환
df = pd.DataFrame(results)
# 통계 정보
total_fonts = len(results)
commercial_free = len([r for r in results if "✅" in r["상업적 사용"]])
needs_check = len([r for r in results if "❓" in r["상업적 사용"]])
commercial_paid = len([r for r in results if "❌" in r["상업적 사용"]])
summary = f"""
## 📊 분석 결과 요약
- **총 폰트 수**: {total_fonts}
- **상업적 무료 사용 가능**: {commercial_free}개 ({commercial_free/total_fonts*100:.1f}%)
- **상업적 라이선스 필요**: {commercial_paid}개 ({commercial_paid/total_fonts*100:.1f}%)
- **라이선스 확인 필요**: {needs_check}개 ({needs_check/total_fonts*100:.1f}%)
### 📈 라이선스 유형별 분포
"""
# 라이선스 유형별 카운트
license_counts = {}
for result in results:
license_type = result["라이선스"]
license_counts[license_type] = license_counts.get(license_type, 0) + 1
for license_type, count in sorted(license_counts.items(), key=lambda x: x[1], reverse=True):
percentage = count / total_fonts * 100
summary += f"- **{license_type}**: {count}개 ({percentage:.1f}%)\n"
summary += f"""
### 💡 권장사항
- **✅ 상업적 무료 폰트**: 별도 라이선스 없이 사용 가능
- **❌ 유료 라이선스 폰트**: 상업적 사용 시 라이선스 구매 필요
- **❓ 확인 필요 폰트**: 제작사 공식 사이트에서 라이선스 확인 권장
⚠️ **주의**: 폰트 라이선스는 변경될 수 있으니 중요한 프로젝트에 사용하기 전에는 반드시 공식 사이트에서 최종 확인하시기 바랍니다.
"""
return df, summary
except Exception as e:
return None, f"분석 중 오류 발생: {str(e)}"
def create_excel_download(df):
"""엑셀 파일 생성"""
try:
# 메모리에서 엑셀 파일 생성
output = io.BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
# 메인 데이터 시트
df.to_excel(writer, sheet_name='폰트 라이선스 정보', index=False)
# 워크시트 가져오기
worksheet = writer.sheets['폰트 라이선스 정보']
# 열 너비 자동 조정
column_widths = {
'A': 25, # 원본 파일명
'B': 20, # 폰트명
'C': 25, # 라이선스
'D': 15, # 상업적 사용
'E': 12, # 출처 표시
'F': 12, # 수정 가능
'G': 20, # 제공처
'H': 40, # 제공처 URL
'I': 50 # 비고
}
for col, width in column_widths.items():
worksheet.column_dimensions[col].width = width
# 요약 정보 시트 추가
summary_data = {
"구분": ["총 폰트 수", "상업적 무료 사용 가능", "유료 라이선스 필요", "라이선스 확인 필요"],
"개수": [
len(df),
len(df[df["상업적 사용"].str.contains("✅", na=False)]),
len(df[df["상업적 사용"].str.contains("❌", na=False)]),
len(df[df["상업적 사용"].str.contains("❓", na=False)])
],
"비율(%)": [
100.0,
len(df[df["상업적 사용"].str.contains("✅", na=False)]) / len(df) * 100,
len(df[df["상업적 사용"].str.contains("❌", na=False)]) / len(df) * 100,
len(df[df["상업적 사용"].str.contains("❓", na=False)]) / len(df) * 100
]
}
summary_df = pd.DataFrame(summary_data)
summary_df.to_excel(writer, sheet_name='요약 통계', index=False)
# 라이선스별 분류 시트
license_summary = df.groupby('라이선스').size().reset_index(name='개수')
license_summary['비율(%)'] = license_summary['개수'] / len(df) * 100
license_summary = license_summary.sort_values('개수', ascending=False)
license_summary.to_excel(writer, sheet_name='라이선스별 통계', index=False)
output.seek(0)
return output.getvalue()
except Exception as e:
print(f"엑셀 생성 오류: {e}")
return None
def process_font_file(file):
"""업로드된 파일 처리"""
if file is None:
return None, "파일을 업로드해주세요.", None
try:
# 파일 내용 읽기
if hasattr(file, 'read'):
content = file.read()
else:
with open(file, 'rb') as f:
content = f.read()
# 폰트 분석
df, summary = analyze_fonts(content)
if df is None:
return None, summary, None
# 엑셀 파일 생성
excel_data = create_excel_download(df)
if excel_data is None:
return df, summary + "\n\n⚠️ 엑셀 파일 생성에 실패했습니다.", None
# 임시 파일로 저장
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
excel_filename = f"font_license_analysis_{timestamp}.xlsx"
# 임시 디렉토리에 저장
temp_dir = tempfile.gettempdir()
excel_path = os.path.join(temp_dir, excel_filename)
with open(excel_path, 'wb') as f:
f.write(excel_data)
return df, summary + f"\n\n✅ 엑셀 파일이 준비되었습니다!", excel_path
except Exception as e:
return None, f"파일 처리 중 오류 발생: {str(e)}", None
# Gradio 인터페이스 생성
def create_app():
with gr.Blocks(
title="한국어 폰트 라이선스 분석기",
theme=gr.themes.Soft()
) as app:
gr.Markdown("""
# 🔍 한국어 폰트 라이선스 분석기
**폰트 목록을 업로드하면 라이선스 정보를 분석하여 엑셀로 제공합니다!**
💼 상업적 사용 가능 여부 | 📄 출처 표시 필요 여부 | ✏️ 수정/재배포 가능 여부 | 🔗 공식 다운로드 링크
""")
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("## 📁 폰트 목록 파일 업로드")
file_input = gr.File(
label="폰트 목록 텍스트 파일 (.txt)",
file_types=[".txt"]
)
analyze_btn = gr.Button(
"🔍 라이선스 분석 시작",
variant="primary",
size="lg"
)
with gr.Accordion("💡 사용 방법", open=True):
gr.Markdown("""
### 📝 간단한 사용법
1. **폰트 목록 텍스트 파일 준비**
- 메모장에서 폰트 파일명을 한 줄에 하나씩 입력
- 예시: `NanumGothic.ttf`, `Pretendard-Regular.otf`
2. **파일 업로드**
- 위에서 만든 txt 파일을 업로드
3. **결과 확인 및 다운로드**
- 분석 결과를 확인하고 엑셀 파일 다운로드
**📝 지원 파일 형식:**
- 한 줄에 하나의 폰트 파일명
- TTF, OTF, TTC 확장자 자동 인식
- UTF-8, CP949, EUC-KR 인코딩 지원
**✨ 예시 파일 내용:**
```
NanumGothic.ttf
Pretendard-Regular.otf
BMDOHYEON_ttf.ttf
SeoulNamsan-Medium.ttf
```
""")
with gr.Column(scale=2):
gr.Markdown("## 📊 분석 결과")
summary_output = gr.Markdown("파일을 업로드하고 '분석 시작' 버튼을 클릭하세요.")
result_table = gr.Dataframe(
headers=["원본 파일명", "폰트명", "라이선스", "상업적 사용", "출처 표시", "수정 가능",