aliceblue11's picture
Update app.py
ffe2f94 verified
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": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"
},
"나눔바른고딕": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "네이버",
"provider_url": "https://hangeul.naver.com/2017/nanum",
"notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"
},
"나눔명조": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "네이버",
"provider_url": "https://hangeul.naver.com/2017/nanum",
"notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"
},
"나눔손글씨": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "네이버",
"provider_url": "https://hangeul.naver.com/2017/nanum",
"notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"
},
"나눔펜": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "네이버",
"provider_url": "https://hangeul.naver.com/2017/nanum",
"notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"
},
"배달의민족 주아": {
"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": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"
},
"서울한강체": {
"license": "SIL OFL 1.1",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "서울시",
"provider_url": "https://www.seoul.go.kr/solution/font.do",
"notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"
},
"Noto Sans Korean": {
"license": "SIL OFL 1.1",
"display_name": "노토 산스 한국어",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "Google",
"provider_url": "https://fonts.google.com/noto",
"notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"
},
"Source Han Sans Korean": {
"license": "SIL OFL 1.1",
"display_name": "본고딕",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "Adobe/Google",
"provider_url": "https://fonts.google.com/noto",
"notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"
},
"Pretendard": {
"license": "SIL OFL 1.1",
"display_name": "프리텐다드",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "Kil Hyung-jin",
"provider_url": "https://github.com/orioncactus/pretendard",
"notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"
},
"SUIT": {
"license": "SIL OFL 1.1",
"display_name": "수트",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "SUNN",
"provider_url": "https://github.com/sunn-us/SUIT",
"notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"
},
"IBM Plex Sans Korean": {
"license": "SIL OFL 1.1",
"display_name": "IBM 플렉스 산스 한국어",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "IBM",
"provider_url": "https://fonts.google.com/specimen/IBM+Plex+Sans+KR",
"notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"
},
"Spoqa Han Sans": {
"license": "SIL OFL 1.1",
"display_name": "스포카 한 산스",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "스포카",
"provider_url": "https://github.com/spoqa/spoqa-han-sans",
"notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"
},
"NEXON Lv1 Gothic": {
"license": "커스텀 무료 라이선스",
"display_name": "넥슨 레벨1 고딕",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "불가능",
"provider": "넥슨",
"provider_url": "https://www.nexon.com/Home/Game/font",
"notes": "상업적 사용 가능, 폰트 자체 판매 금지"
},
"NEXON Lv2 Gothic": {
"license": "커스텀 무료 라이선스",
"display_name": "넥슨 레벨2 고딕",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "불가능",
"provider": "넥슨",
"provider_url": "https://www.nexon.com/Home/Game/font",
"notes": "상업적 사용 가능, 폰트 자체 판매 금지"
},
"TmoneyRoundWind": {
"license": "커스텀 무료 라이선스",
"display_name": "티머니 라운드윈드",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "불가능",
"provider": "티몬",
"provider_url": "https://brunch.co.kr/@creative/32",
"notes": "상업적 사용 가능, 폰트 자체 판매 금지"
},
"한국관광공사체": {
"license": "공공누리 제1유형",
"commercial_free": "✅ 가능",
"attribution": "불필요",
"modification": "가능",
"provider": "한국관광공사",
"provider_url": "https://kto.visitkorea.or.kr/kor/notice/data/storybook/font.kto",
"notes": "공공누리 제1유형: 출처표시 조건으로 자유이용 가능"
},
"KoPubWorld돋움체": {
"license": "공공누리 제1유형",
"commercial_free": "✅ 가능",
"attribution": "필요",
"modification": "가능",
"provider": "한국출판인쇄문화협회",
"provider_url": "http://www.kopus.org/biz/electronic/font.aspx",
"notes": "공공누리 제1유형: 출처표시 조건으로 자유이용 가능"
},
"윤고딕": {
"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": "개인 사용만 무료, 상업적 사용 시 유료 라이선스 구매 필요"
}
}
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)',
r'(.+?)[-_\s]?\d+',
r'(.+?)[-_\s]?(Kr|KR|Korean)',
]
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)
# 정확한 매칭
if cleaned_name in FONT_LICENSE_DB:
info = FONT_LICENSE_DB[cleaned_name].copy()
# 한글 표시명이 있으면 사용
if 'display_name' in info:
info['display_font_name'] = info['display_name']
else:
info['display_font_name'] = cleaned_name
return info
# 부분 매칭
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():
info_copy = info.copy()
if 'display_name' in info_copy:
info_copy['display_font_name'] = info_copy['display_name']
else:
info_copy['display_font_name'] = db_font
return info_copy
# 패턴 매칭 (재검증됨)
special_patterns = {
r'nanum|나눔': "나눔고딕",
r'baemin|배민|배달의민족': "배달의민족 주아",
r'seoul|서울': "서울남산체",
r'pretendard': "Pretendard",
r'noto.*sans.*kr|noto.*sans.*korean': "Noto Sans Korean",
r'source.*han.*sans|본고딕': "Source Han Sans Korean",
r'ibm.*plex.*sans.*kr|ibm.*plex.*sans.*korean': "IBM Plex Sans Korean",
r'spoqa.*han.*sans': "Spoqa Han Sans",
r'nexon.*lv1|nexon.*level.*1': "NEXON Lv1 Gothic",
r'nexon.*lv2|nexon.*level.*2': "NEXON Lv2 Gothic",
r'tmoney.*round.*wind': "TmoneyRoundWind",
r'suit': "SUIT",
r'yoon|윤': "윤고딕",
r'sandoll|산돌': "산돌고딕",
r'kopub.*world': "KoPubWorld돋움체",
r'한국관광공사': "한국관광공사체"
}
for pattern, matched_font in special_patterns.items():
if re.search(pattern, cleaned_name.lower()):
if matched_font in FONT_LICENSE_DB:
info_copy = FONT_LICENSE_DB[matched_font].copy()
if 'display_name' in info_copy:
info_copy['display_font_name'] = info_copy['display_name']
else:
info_copy['display_font_name'] = matched_font
return info_copy
# 유사도 매칭
matches = get_close_matches(cleaned_name, FONT_LICENSE_DB.keys(), n=1, cutoff=0.6)
if matches:
info_copy = FONT_LICENSE_DB[matches[0]].copy()
if 'display_name' in info_copy:
info_copy['display_font_name'] = info_copy['display_name']
else:
info_copy['display_font_name'] = matches[0]
return info_copy
# 기본값
return {
"license": "❓ 확인 필요",
"commercial_free": "❓ 확인 필요",
"attribution": "❓ 확인 필요",
"modification": "❓ 확인 필요",
"provider": "❓ 확인 필요",
"provider_url": "",
"display_font_name": cleaned_name,
"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('#'):
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 font_list[0].startswith("파일 파싱 오류"):
return None, "파일을 읽을 수 없습니다."
results = []
for font_file in font_list:
font_name = clean_font_name(font_file)
license_info = find_font_license(font_name)
# 한글 폰트명 사용
display_name = license_info.get('display_font_name', font_name)
results.append({
"원본 파일명": font_file,
"폰트명": display_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"]
})
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}%)
✅ **엑셀 파일이 준비되었습니다!**
"""
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, 'I': 50
}
for col, width in column_widths.items():
worksheet.column_dimensions[col].width = width
output.seek(0)
return output.getvalue()
except Exception as 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, 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, excel_path
except Exception as e:
return None, f"파일 처리 오류: {str(e)}", None
def create_app():
with gr.Blocks(title="한국어 폰트 라이선스 분석기") 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")
gr.Markdown("""
### 사용 방법
1. 메모장에서 폰트 파일명을 한 줄에 하나씩 입력
2. txt 파일로 저장 후 업로드
3. 분석 결과 확인 및 엑셀 다운로드
**예시 파일 내용:**
```
NanumGothic.ttf
Pretendard-Regular.otf
BMDOHYEON_ttf.ttf
```
""")
with gr.Column(scale=2):
gr.Markdown("## 📊 분석 결과")
summary_output = gr.Markdown("파일을 업로드하세요.")
result_table = gr.Dataframe(
headers=["원본 파일명", "폰트명", "라이선스", "상업적 사용", "출처 표시", "수정 가능", "제공처", "제공처 URL", "비고"],
label="폰트 라이선스 정보",
column_widths=["15%", "12%", "15%", "10%", "8%", "8%", "12%", "20%", "20%"],
interactive=True
)
excel_download = gr.File(label="📥 엑셀 다운로드", visible=False)
with gr.Accordion("📋 샘플 결과", open=False):
sample_data = [
["NanumGothic.ttf", "나눔고딕", "SIL OFL 1.1", "✅ 가능", "불필요", "가능", "네이버", "https://hangeul.naver.com/2017/nanum", "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"],
["Pretendard-Regular.otf", "프리텐다드", "SIL OFL 1.1", "✅ 가능", "불필요", "가능", "Kil Hyung-jin", "https://github.com/orioncactus/pretendard", "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"],
["YoonGothic.ttf", "윤고딕", "상업적 라이선스 필요", "❌ 유료", "해당없음", "라이선스에 따라", "윤디자인그룹", "https://www.yoondesign.com", "개인 사용만 무료, 상업적 사용 시 유료 라이선스 구매 필요"],
["KoPubWorldDotum.ttf", "KoPubWorld돋움체", "공공누리 제1유형", "✅ 가능", "필요", "가능", "한국출판인쇄문화협회", "http://www.kopus.org/biz/electronic/font.aspx", "공공누리 제1유형: 출처표시 조건으로 자유이용 가능"]
]
gr.Dataframe(
value=sample_data,
headers=["원본 파일명", "폰트명", "라이선스", "상업적 사용", "출처 표시", "수정 가능", "제공처", "제공처 URL", "비고"],
column_widths=["15%", "12%", "15%", "10%", "8%", "8%", "12%", "20%", "20%"]
)
with gr.Accordion("ℹ️ 라이선스 유형 설명", open=False):
gr.Markdown("""
### 주요 라이선스 유형 (재검증됨)
**✅ SIL OFL 1.1 (Open Font License)**
- 상업적 사용: 완전 자유
- 출처 표시: 불필요
- 수정/재배포: 가능
- 웹폰트 사용: 가능
**✅ 커스텀 무료 라이선스**
- 각 제작사별 고유 조건
- 대부분 상업적 사용 가능
- 일부 제한사항 존재 (BI 사용 금지 등)
**✅ 공공누리 제1유형**
- 공공기관에서 제작한 폰트
- 출처표시 조건으로 자유이용 가능
- 상업적 사용 가능
**❌ 상업적 라이선스 필요**
- 개인 사용: 무료
- 상업적 사용: 유료 라이선스 구매 필요
- 주로 전문 폰트 제작사 폰트들
**❓ 확인 필요**
- 데이터베이스에 정보가 없는 폰트
- 제작사 공식 사이트에서 직접 확인 필요
""")
gr.Markdown("### ⚠️ 안내사항")
gr.Markdown("- 참고용 도구입니다. 상업적 사용 전 공식 사이트에서 최종 확인하세요.")
gr.Markdown("- 라이선스 정보는 재검증되었으나 변경될 수 있습니다.")
gr.Markdown("- 총 25개 주요 한국어 폰트 정보를 제공합니다.")
def handle_analysis(file):
if file is None:
return "파일을 업로드해주세요.", None, gr.File(visible=False)
df, summary, excel_file = process_font_file(file)
if df is None:
return summary, None, gr.File(visible=False)
if excel_file:
return summary, df, gr.File(value=excel_file, visible=True)
else:
return summary, df, gr.File(visible=False)
analyze_btn.click(
fn=handle_analysis,
inputs=file_input,
outputs=[summary_output, result_table, excel_download]
)
file_input.change(
fn=handle_analysis,
inputs=file_input,
outputs=[summary_output, result_table, excel_download]
)
return app
if __name__ == "__main__":
app = create_app()
app.launch()